diff --git a/.github/workflows/stable-ids.yml b/.github/workflows/stable-ids.yml new file mode 100644 index 0000000..0efe2eb --- /dev/null +++ b/.github/workflows/stable-ids.yml @@ -0,0 +1,39 @@ +# Stability gate for stable_id (engine#90): compares the PR head against the +# PR base, so a disappeared/reused id or a coverage regression fails BEFORE +# merge. Self-test first: a gate whose detectors were never seen firing is a +# blind gate. +name: stable-id gate + +on: + pull_request: + +jobs: + stable-ids: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: actions/setup-python@v6 + with: + python-version: "3.12" + + - uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: Install the pinned engine (the gate ships with it) + run: npm install --no-save --no-package-lock --no-audit --no-fund "learn-content-engine@$(cat schema/engine-version.txt)" + + - name: Install PyYAML + run: pip install "pyyaml>=6,<7" + + - name: Stability gate against the PR base (shipped command) + run: npx --no-install learn-content-engine check-stable-ids --base origin/${{ github.base_ref }} + + - name: Coverage ratchet self-test (every path must fire) + run: python scripts/check_stable_id_coverage.py --self-test + + - name: Coverage ratchet + run: python scripts/check_stable_id_coverage.py diff --git a/Makefile b/Makefile index 7e9e8f4..568d41c 100644 --- a/Makefile +++ b/Makefile @@ -91,3 +91,8 @@ audit: $(VENV)/.ready clean: rm -rf $(VENV) + +stable-ids: $(ENGINE_STAMP) ## Stabilitaets-Gate (mitgeliefert) + Abdeckungs-Ratchet (repo-lokal) + npx --no-install learn-content-engine check-stable-ids --base origin/main + python3 scripts/check_stable_id_coverage.py --self-test + python3 scripts/check_stable_id_coverage.py diff --git a/schema/content-manifest.schema.json b/schema/content-manifest.schema.json index fad05bc..9a5392f 100644 --- a/schema/content-manifest.schema.json +++ b/schema/content-manifest.schema.json @@ -6,7 +6,7 @@ "properties": { "assets": { "default": [], - "description": "Phase 54 / v1.37.0 \u2014 optional list of binary assets (images, audio) the set bundles. Each entry declares a relative path inside ``assets/`` and the expected size in KiB. The downloader fetches every declared asset alongside the lesson JSON; the cache stores them under ``{cache_root}/.../v{version}/assets/{path}``. The manifest validator rejects assets exceeding the per-file size limit (default 500 KiB).", + "description": "Phase 54 / v1.37.0 — optional list of binary assets (images, audio) the set bundles. Each entry declares a relative path inside ``assets/`` and the expected size in KiB. The downloader fetches every declared asset alongside the lesson JSON; the cache stores them under ``{cache_root}/.../v{version}/assets/{path}``. The manifest validator rejects assets exceeding the per-file size limit (default 500 KiB).", "items": { "$ref": "#/$defs/ContentSetAsset" }, @@ -24,7 +24,7 @@ } ], "default": null, - "description": "#769 \u2014 optional set-level book block (title/author/url/asin). When present, the lesson's 'Vertiefe das Thema' section auto-inserts it as the first media item." + "description": "#769 — optional set-level book block (title/author/url/asin). When present, the lesson's 'Vertiefe das Thema' section auto-inserts it as the first media item." }, "cover_image": { "anyOf": [ @@ -76,7 +76,7 @@ "title": "Domain Label" }, "id": { - "description": "Slug-safe identifier, unique within the manifest. Convention (Phase 60 / v1.44.0): ``{target}-{level}-from-{source}`` for language sets (e.g. ``fr-a1-from-de``). Pre-v1.2 ids like ``language-fr-a1`` still load \u2014 the loader does NOT parse this, it's free-form per the EXP-005 domain-agnostic stance.", + "description": "Slug-safe identifier, unique within the manifest. Convention (Phase 60 / v1.44.0): ``{target}-{level}-from-{source}`` for language sets (e.g. ``fr-a1-from-de``). Pre-v1.2 ids like ``language-fr-a1`` still load — the loader does NOT parse this, it's free-form per the EXP-005 domain-agnostic stance.", "maxLength": 120, "minLength": 1, "title": "Id", @@ -107,12 +107,12 @@ } ], "default": null, - "description": "Phase 60 / v1.44.0 \u2014 repo-relative directory where the set's own ``manifest.yaml`` + ``lessons/`` + ``assets/`` live. Enables the source-language tree (e.g. ``sets/de/fr-a1`` for a French-for-German set while the id stays the flat slug ``fr-a1-from-de``). When omitted the loader falls back to the legacy ``sets/{id}`` convention. No leading/trailing slash, no ``..`` segments.", + "description": "Phase 60 / v1.44.0 — repo-relative directory where the set's own ``manifest.yaml`` + ``lessons/`` + ``assets/`` live. Enables the source-language tree (e.g. ``sets/de/fr-a1`` for a French-for-German set while the id stays the flat slug ``fr-a1-from-de``). When omitted the loader falls back to the legacy ``sets/{id}`` convention. No leading/trailing slash, no ``..`` segments.", "title": "Path" }, "source_language": { "default": "en", - "description": "BCP-47 code of the language the learner ALREADY SPEAKS \u2014 the language the card ``back`` fields, notes and theory text are written in. A 'French A1 for German speakers' set has ``target_language: fr`` + ``source_language: de``. Defaults to ``en`` for pre-v1.2 content (the pilot sets were authored with English explanations).", + "description": "BCP-47 code of the language the learner ALREADY SPEAKS — the language the card ``back`` fields, notes and theory text are written in. A 'French A1 for German speakers' set has ``target_language: fr`` + ``source_language: de``. Defaults to ``en`` for pre-v1.2 content (the pilot sets were authored with English explanations).", "title": "Source Language", "type": "string" }, @@ -132,7 +132,7 @@ "type": "string" }, "title": { - "description": "Human-readable title shown in the Set Browser, in the learner's SOURCE language (e.g. 'Franz\u00f6sisch A1 f\u00fcr Deutschsprachige' for a fr-from-de set).", + "description": "Human-readable title shown in the Set Browser, in the learner's SOURCE language (e.g. 'Französisch A1 für Deutschsprachige' for a fr-from-de set).", "maxLength": 200, "minLength": 1, "title": "Title", @@ -149,7 +149,7 @@ } ], "default": null, - "description": "Phase 60 / v1.44.0 \u2014 optional title in the TARGET language (e.g. 'Fran\u00e7ais A1' for a French set). Shown as a secondary native-script label alongside ``title``. The community-share validator requires it for shareable sets; bundled/legacy sets may omit it.", + "description": "Phase 60 / v1.44.0 — optional title in the TARGET language (e.g. 'Français A1' for a French set). Shown as a secondary native-script label alongside ``title``. The community-share validator requires it for shareable sets; bundled/legacy sets may omit it.", "title": "Title Native" }, "version": { @@ -166,6 +166,71 @@ ], "title": "Visibility", "type": "string" + }, + "attribution": { + "anyOf": [ + { + "type": "object", + "additionalProperties": false, + "required": [ + "author" + ], + "properties": { + "author": { + "type": "string", + "minLength": 1, + "maxLength": 120, + "title": "Author", + "description": "Display name of the person this set's content is attributed to. Attribution, NOT authorization: without accounts or a server the name is unverifiable, and the field claims nothing more." + }, + "derived_from": { + "type": "array", + "maxItems": 8, + "title": "Derived From", + "description": "Bounded derivation chain, OLDEST first. Whoever edits and re-shares a foreign set moves the previous author to the END of this list and sets themselves as author; when the list is full, the FIRST entry (the origin) stays and the oldest middle entry is dropped (editor/author rule; the schema enforces only the bound).", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "author" + ], + "properties": { + "author": { + "type": "string", + "minLength": 1, + "maxLength": 120, + "title": "Author" + } + } + } + } + } + }, + { + "type": "null" + } + ], + "default": null, + "title": "Attribution", + "description": "engine#90 - schema 1.9 (additive). Content attribution for the set: who wrote it, and the bounded chain it was derived through. PERSONAL DATA: the name travels with the set when it is shared; a consumer app must point that out before it becomes visible (the contributed_by opt-in pattern). Distinct from and never merged with: 'book' (source material), 'ai_validation' (review provenance in free-form metadata), repo-level 'metadata.author' (repo operator) and the lesson-level 'contributed_by' (per-lesson credit)." + }, + "review_status": { + "anyOf": [ + { + "type": "string", + "enum": [ + "authored", + "generated", + "reviewed" + ] + }, + { + "type": "null" + } + ], + "default": null, + "title": "Review Status", + "description": "engine#94 - schema 1.9 (additive). Three-state review standing derived from ORIGIN, because origin is what makes a set review-worthy: 'authored' = hand-written by a speaker/domain expert, no review required; 'generated' = machine-generated (AI/book/analysis), native-speaker or expert review PENDING; 'reviewed' = machine-generated and reviewed. Absent means 'authored' (legacy hand-written content). Consumers derive 'advertisable as reviewed' as status != 'generated'. Distinct from 'visibility' (display hint, never a quality statement) and from 'ai_validation' (AI check provenance)." } }, "required": [ @@ -181,17 +246,17 @@ }, "ContentSetAsset": { "additionalProperties": false, - "description": "One bundled binary asset (image, audio) declared in the\nset manifest (Phase 54 / v1.37.0).\n\nThe declaration drives:\n - the downloader (fetch_asset per ``path`` alongside the\n lesson JSON)\n - the cache writer (store under ``assets/{path}``)\n - the size validator (reject ``size_kb > MAX_ASSET_SIZE_KB``)\n\nOptional everywhere \u2014 sets without any assets simply\nomit the ``assets`` list. The current PictureChoice\ncomponent falls back to text-only when the resolver\ncan't produce a blob URL, so authored content without\nassets stays playable.", + "description": "One bundled binary asset (image, audio) declared in the\nset manifest (Phase 54 / v1.37.0).\n\nThe declaration drives:\n - the downloader (fetch_asset per ``path`` alongside the\n lesson JSON)\n - the cache writer (store under ``assets/{path}``)\n - the size validator (reject ``size_kb > MAX_ASSET_SIZE_KB``)\n\nOptional everywhere — sets without any assets simply\nomit the ``assets`` list. The current PictureChoice\ncomponent falls back to text-only when the resolver\ncan't produce a blob URL, so authored content without\nassets stays playable.", "properties": { "path": { - "description": "Relative path inside the set's ``assets/`` directory. Example: ``img/sunrise.png`` resolves to ``{cache_root}/.../assets/img/sunrise.png``. No leading slash, no ``..`` segments \u2014 the path is appended to a Path() and any upward navigation would escape the cache isolation.", + "description": "Relative path inside the set's ``assets/`` directory. Example: ``img/sunrise.png`` resolves to ``{cache_root}/.../assets/img/sunrise.png``. No leading slash, no ``..`` segments — the path is appended to a Path() and any upward navigation would escape the cache isolation.", "maxLength": 300, "minLength": 1, "title": "Path", "type": "string" }, "size_kb": { - "description": "Declared file size in KiB (used by the validator + the downloader's progress reporting). The downloader rejects assets whose actual byte length exceeds ``size_kb * 1024`` by more than 10 percent \u2014 keeps content authors honest.", + "description": "Declared file size in KiB (used by the validator + the downloader's progress reporting). The downloader rejects assets whose actual byte length exceeds ``size_kb * 1024`` by more than 10 percent — keeps content authors honest.", "maximum": 500, "minimum": 1, "title": "Size Kb", @@ -206,7 +271,7 @@ "type": "object" }, "ContentSetBook": { - "description": "#769 \u2014 optional set-level book block (manifest ``sets[].book``).\n\nSurfaced to the lesson's \"Vertiefe das Thema\" section as the first\nmedia item. ``extra=\"ignore\"`` tolerates future fields (e.g. ``isbn``,\n``year``) the media card doesn't consume.", + "description": "#769 — optional set-level book block (manifest ``sets[].book``).\n\nSurfaced to the lesson's \"Vertiefe das Thema\" section as the first\nmedia item. ``extra=\"ignore\"`` tolerates future fields (e.g. ``isbn``,\n``year``) the media card doesn't consume.", "properties": { "asin": { "anyOf": [ @@ -283,7 +348,7 @@ "metadata": { "additionalProperties": true, "default": {}, - "description": "Free-form repo-level metadata (license, author, homepage URL, contact). The loader does not interpret these fields \u2014 they surface as-is in the Set Browser's 'About this source' panel.", + "description": "Free-form repo-level metadata (license, author, homepage URL, contact). The loader does not interpret these fields — they surface as-is in the Set Browser's 'About this source' panel.", "title": "Metadata", "type": "object" }, @@ -315,5 +380,5 @@ ], "title": "ContentManifest", "type": "object", - "x-schema-version": "1.8" + "x-schema-version": "1.9" } diff --git a/schema/engine-version.txt b/schema/engine-version.txt index a803cc2..2a0970c 100644 --- a/schema/engine-version.txt +++ b/schema/engine-version.txt @@ -1 +1 @@ -0.14.0 +0.16.1 diff --git a/schema/lesson.schema.json b/schema/lesson.schema.json index c00069d..ca8be1e 100644 --- a/schema/lesson.schema.json +++ b/schema/lesson.schema.json @@ -2,7 +2,7 @@ "$defs": { "Card": { "additionalProperties": false, - "description": "The smallest learnable unit (Phase 43 / 2B-lesson).\n\nA card carries a single term / concept / fact in a single\ndirection. SRS (Phase 46) tracks one card at a time;\nindividual exercises reference cards by id so a single\n'Bonjour = Hello' card can drive a matching exercise, a\nfree-text drill, and a summary review without\nduplication.\n\nConvention: ``card.id`` is unique within the lesson, not\nglobally. Cross-lesson card sharing happens via a\nseparate ``shared/`` directory inside the set (P-111\nterritory \u2014 not yet implemented).", + "description": "The smallest learnable unit (Phase 43 / 2B-lesson).\n\nA card carries a single term / concept / fact in a single\ndirection. SRS (Phase 46) tracks one card at a time;\nindividual exercises reference cards by id so a single\n'Bonjour = Hello' card can drive a matching exercise, a\nfree-text drill, and a summary review without\nduplication.\n\nConvention: ``card.id`` is unique within the lesson, not\nglobally. Cross-lesson card sharing happens via a\nseparate ``shared/`` directory inside the set (P-111\nterritory — not yet implemented).", "properties": { "audio": { "anyOf": [ @@ -109,6 +109,20 @@ "title": "Id", "type": "string" }, + "stable_id": { + "anyOf": [ + { + "type": "string", + "pattern": "^[a-z0-9][a-z0-9_-]{7,63}$" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Stable Id", + "description": "engine#90 - schema 1.9 (additive). Author-owned, version-stable identity for progress/SRS joins: once published it NEVER changes, set-wide unique (cross-lesson uniqueness is checked by the repo gate via collectStableIds; the schema sees one document). Opaque mint-once value (lowercase slug, 8-64 chars), NOT derived from content, so answer-text fixes do not move it. Optional: pre-1.9 content validates unchanged. SCOPE: this closes orphaning by slug rename or position shift on the exercise/card level; it does NOT close the element-level case (an answer correction inside a surviving exercise still moves the content-derived element key, engine#91)." + }, "image": { "anyOf": [ { @@ -152,7 +166,7 @@ } ], "default": null, - "description": "Optional Markdown footnote shown after the user answers. Pronunciation tips, etymology, false-friend warnings \u2014 anything that helps long-term retention.", + "description": "Optional Markdown footnote shown after the user answers. Pronunciation tips, etymology, false-friend warnings — anything that helps long-term retention.", "title": "Notes" }, "tags": { @@ -193,7 +207,7 @@ }, "CardTokenRole": { "additionalProperties": false, - "description": "One ``token \u2192 role`` annotation on a card.\n\nPhase 52I / v1.35.0 / P-130. The cloze generator looks up\nits target blank by matching ``token`` against the\n``ElementError.element_key`` \u2014 when a role is present, the\ngenerator can pick a same-role distractor pool instead of\na position-based heuristic.\n\nThe ``token`` is a verbatim slice of the card's ``front``;\nno whitespace normalisation, so authors can annotate even\nsub-word morphemes (an accent-bearing letter, an article\ncontraction) if a future generator needs it.", + "description": "One ``token → role`` annotation on a card.\n\nPhase 52I / v1.35.0 / P-130. The cloze generator looks up\nits target blank by matching ``token`` against the\n``ElementError.element_key`` — when a role is present, the\ngenerator can pick a same-role distractor pool instead of\na position-based heuristic.\n\nThe ``token`` is a verbatim slice of the card's ``front``;\nno whitespace normalisation, so authors can annotate even\nsub-word morphemes (an accent-bearing letter, an article\ncontraction) if a future generator needs it.", "properties": { "role": { "$ref": "#/$defs/TokenRole", @@ -216,7 +230,7 @@ }, "ClozeBlank": { "additionalProperties": false, - "description": "One blank inside a cloze exercise's ``sentence`` (Phase 52D /\nv1.35.0 / P-127).\n\nMarker-based convention: the sentence carries visible ``___``\ntokens; ``blanks[i]`` provides the metadata for the i-th\nmarker (left-to-right). The validator enforces\n``sentence.count(\"___\") == len(blanks)`` so the i\u2194i mapping\nis unambiguous at render time.\n\n``accept`` carries the per-blank canonical + acceptable\nvariants \u2014 the renderer reuses FreeText's ``isFreeTextCorrect``\nmatcher (NFC-normalised + Levenshtein <= 1) so authors only\nneed to enumerate semantic variants (gendered article,\ncapitalisation, et cetera), not typos.", + "description": "One blank inside a cloze exercise's ``sentence`` (Phase 52D /\nv1.35.0 / P-127).\n\nMarker-based convention: the sentence carries visible ``___``\ntokens; ``blanks[i]`` provides the metadata for the i-th\nmarker (left-to-right). The validator enforces\n``sentence.count(\"___\") == len(blanks)`` so the i↔i mapping\nis unambiguous at render time.\n\n``accept`` carries the per-blank canonical + acceptable\nvariants — the renderer reuses FreeText's ``isFreeTextCorrect``\nmatcher (NFC-normalised + Levenshtein <= 1) so authors only\nneed to enumerate semantic variants (gendered article,\ncapitalisation, et cetera), not typos.", "properties": { "accept": { "description": "Accepted answers for this blank. First entry is the canonical (shown after a wrong attempt). Same shape as FREE_TEXT.accept.", @@ -416,6 +430,20 @@ "title": "Id", "type": "string" }, + "stable_id": { + "anyOf": [ + { + "type": "string", + "pattern": "^[a-z0-9][a-z0-9_-]{7,63}$" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Stable Id", + "description": "engine#90 - schema 1.9 (additive). Author-owned, version-stable identity for progress/SRS joins: once published it NEVER changes, set-wide unique (cross-lesson uniqueness is checked by the repo gate via collectStableIds; the schema sees one document). Opaque mint-once value (lowercase slug, 8-64 chars), NOT derived from content, so answer-text fixes do not move it. Optional: pre-1.9 content validates unchanged. SCOPE: this closes orphaning by slug rename or position shift on the exercise/card level; it does NOT close the element-level case (an answer correction inside a surviving exercise still moves the content-derived element key, engine#91)." + }, "images": { "anyOf": [ { @@ -529,7 +557,7 @@ "type": "object" }, "ExerciseType": { - "description": "Closed enum of exercise types the loader knows about.\n\nEXP-001 + EXP-006: the four base types ship in Phase 43-45.\nPhase 52D / v1.35.0 added CLOZE (fill-in-the-blank with\n``___`` markers) \u2014 see the schema_version bump in\n``models.py``. Adding a sixth type (ordering, drag-image-\npair, etc.) requires a minor schema_version bump and a new\nenum value plus its renderer.", + "description": "Closed enum of exercise types the loader knows about.\n\nEXP-001 + EXP-006: the four base types ship in Phase 43-45.\nPhase 52D / v1.35.0 added CLOZE (fill-in-the-blank with\n``___`` markers) — see the schema_version bump in\n``models.py``. Adding a sixth type (ordering, drag-image-\npair, etc.) requires a minor schema_version bump and a new\nenum value plus its renderer.", "enum": [ "matching", "picture_choice", @@ -549,7 +577,7 @@ }, "InlineExample": { "additionalProperties": false, - "description": "One inline worked example on a theory step or exercise (schema v1.5).\n\nAn inline example carries REAL content the learner reads in place \u2014\na sample sentence (language lessons) or a code snippet with syntax\nhighlighting (programming lessons). This is DISTINCT from\n``LessonStep.example_url`` (#139 / schema v1.4), which links OUT to an\nexternal illustration: ``example_url`` is the LINK variant,\n``examples`` is the INLINE-CONTENT variant. The two are complementary\nand may coexist on the same theory step.\n\nWhen ``language`` is set, ``content`` is treated as source code in\nthat language and rendered as a syntax-highlighted block (the same\n``CodeBlock`` the theory Markdown + code cards use); when it is\nabsent, ``content`` is plain text. Additive + optional, so content\nwithout ``examples`` validates unchanged.", + "description": "One inline worked example on a theory step or exercise (schema v1.5).\n\nAn inline example carries REAL content the learner reads in place —\na sample sentence (language lessons) or a code snippet with syntax\nhighlighting (programming lessons). This is DISTINCT from\n``LessonStep.example_url`` (#139 / schema v1.4), which links OUT to an\nexternal illustration: ``example_url`` is the LINK variant,\n``examples`` is the INLINE-CONTENT variant. The two are complementary\nand may coexist on the same theory step.\n\nWhen ``language`` is set, ``content`` is treated as source code in\nthat language and rendered as a syntax-highlighted block (the same\n``CodeBlock`` the theory Markdown + code cards use); when it is\nabsent, ``content`` is plain text. Additive + optional, so content\nwithout ``examples`` validates unchanged.", "properties": { "content": { "description": "The example's content. Plain text (e.g. a sample sentence) when ``language`` is absent; source code in ``language`` when it is set.", @@ -595,7 +623,7 @@ }, "LessonResource": { "additionalProperties": false, - "description": "One lesson-level supplementary-media entry (EXP-029 / MED-05).\n\nMirrors a ``media.yaml`` resource minus ``domain`` (inherited\nfrom the parent set). Surfaced in the \"Vertiefe das Thema\"\nsection after the lesson summary. Optional + additive, so\npre-EXP-029 lessons load unchanged. Added to the authoritative\nschema (EXP-039) so the JSON-Schema / generated TS types cover\nit \u2014 previously this shape lived only in the frontend\n``ContentLessonResource`` interface, and a lesson carrying\n``resources`` was rejected by ``extra=\"forbid\"`` here.", + "description": "One lesson-level supplementary-media entry (EXP-029 / MED-05).\n\nMirrors a ``media.yaml`` resource minus ``domain`` (inherited\nfrom the parent set). Surfaced in the \"Vertiefe das Thema\"\nsection after the lesson summary. Optional + additive, so\npre-EXP-029 lessons load unchanged. Added to the authoritative\nschema (EXP-039) so the JSON-Schema / generated TS types cover\nit — previously this shape lived only in the frontend\n``ContentLessonResource`` interface, and a lesson carrying\n``resources`` was rejected by ``extra=\"forbid\"`` here.", "properties": { "author": { "anyOf": [ @@ -791,7 +819,7 @@ } ], "default": null, - "description": "THEORY: optional inline worked examples rendered under the step body (schema v1.5, additive). DISTINCT from ``example_url``: that links OUT to an external illustration, ``examples`` carries the example content INLINE (a sample sentence, or a syntax-highlighted code snippet \u2014 see ``InlineExample.language``). The two may coexist on one step. Additive + optional; steps without ``examples`` validate unchanged.", + "description": "THEORY: optional inline worked examples rendered under the step body (schema v1.5, additive). DISTINCT from ``example_url``: that links OUT to an external illustration, ``examples`` carries the example content INLINE (a sample sentence, or a syntax-highlighted code snippet — see ``InlineExample.language``). The two may coexist on one step. Additive + optional; steps without ``examples`` validate unchanged.", "title": "Examples" }, "exercise": { @@ -893,7 +921,7 @@ }, "Pair": { "additionalProperties": false, - "description": "One left\u2194right pair in a MATCHING exercise.\n\nEXP-039: modeled explicitly (was an inline ``dict[str, str]``)\nso the generated JSON-Schema / TS types carry the structured\n``{left, right}`` shape instead of a loose string map. The\n``extra=\"forbid\"`` config + the two required fields replace the\nformer per-pair key check in ``_validate_matching_fields``;\nvalidation semantics are unchanged (a pair must have exactly\n``left`` and ``right``).", + "description": "One left↔right pair in a MATCHING exercise.\n\nEXP-039: modeled explicitly (was an inline ``dict[str, str]``)\nso the generated JSON-Schema / TS types carry the structured\n``{left, right}`` shape instead of a loose string map. The\n``extra=\"forbid\"`` config + the two required fields replace the\nformer per-pair key check in ``_validate_matching_fields``;\nvalidation semantics are unchanged (a pair must have exactly\n``left`` and ``right``).", "properties": { "left": { "description": "The left-column item. The renderer shuffles before display.", @@ -976,7 +1004,7 @@ "type": "string" }, "TokenRole": { - "description": "Closed enum of grammatical roles a card token can carry.\n\nPhase 52I / v1.35.0 / P-130. Annotates individual tokens\ninside a card's ``front`` so the v1.35.0+ cloze generator\ncan pick a semantically-meaningful blank instead of a\nposition-based one. Optional field on Card \u2014 old content\nwithout token_roles still validates and the generator\nfalls back to a positional heuristic.\n\nClosed enum to keep author input disciplined. Adding a\nrole (e.g. ``pronoun``, ``conjunction``, ``auxiliary``)\nis a minor schema_version bump \u2014 extending an open enum\nsilently would let typos masquerade as valid roles and\nthe generator would skip them without warning.", + "description": "Closed enum of grammatical roles a card token can carry.\n\nPhase 52I / v1.35.0 / P-130. Annotates individual tokens\ninside a card's ``front`` so the v1.35.0+ cloze generator\ncan pick a semantically-meaningful blank instead of a\nposition-based one. Optional field on Card — old content\nwithout token_roles still validates and the generator\nfalls back to a positional heuristic.\n\nClosed enum to keep author input disciplined. Adding a\nrole (e.g. ``pronoun``, ``conjunction``, ``auxiliary``)\nis a minor schema_version bump — extending an open enum\nsilently would let typos masquerade as valid roles and\nthe generator would skip them without warning.", "enum": [ "article", "verb", @@ -993,7 +1021,7 @@ "$id": "https://astrapi69.github.io/learn-content-engine/schema/lesson.schema.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, - "description": "One lesson in a content set (Phase 43 / 2B-lesson).\n\nA lesson is the unit a user works through end-to-end \u2014\ntypically 5-15 minutes of content. The viewer (Phase 44)\nwalks the steps in order; SRS (Phase 46) tracks the\ncards referenced by each exercise.\n\nReferential integrity: every ``card_id`` referenced by\nany exercise step MUST exist in the lesson's ``cards``\nlist. Enforced by the model validator so the viewer can\ntrust the references later.", + "description": "One lesson in a content set (Phase 43 / 2B-lesson).\n\nA lesson is the unit a user works through end-to-end —\ntypically 5-15 minutes of content. The viewer (Phase 44)\nwalks the steps in order; SRS (Phase 46) tracks the\ncards referenced by each exercise.\n\nReferential integrity: every ``card_id`` referenced by\nany exercise step MUST exist in the lesson's ``cards``\nlist. Enforced by the model validator so the viewer can\ntrust the references later.", "properties": { "cards": { "default": [], @@ -1068,7 +1096,7 @@ "type": "integer" }, "id": { - "description": "Slug-safe id, unique within the parent set. Convention: ``NN-slug`` (e.g. ``01-greetings``) for deterministic ordering, though the loader does not enforce ordering \u2014 it reads the set's manifest for the lesson sequence.", + "description": "Slug-safe id, unique within the parent set. Convention: ``NN-slug`` (e.g. ``01-greetings``) for deterministic ordering, though the loader does not enforce ordering — it reads the set's manifest for the lesson sequence.", "maxLength": 120, "minLength": 1, "title": "Id", @@ -1131,7 +1159,7 @@ } ], "default": null, - "description": "Optional BCP-47 code of the language taught (Phase 60 / v1.44.0). Mirrors the parent set's ``target_language``; lets an exported standalone lesson carry its own pair. Absent on pre-v1.2 lessons \u2014 the parent set is authoritative.", + "description": "Optional BCP-47 code of the language taught (Phase 60 / v1.44.0). Mirrors the parent set's ``target_language``; lets an exported standalone lesson carry its own pair. Absent on pre-v1.2 lessons — the parent set is authoritative.", "title": "Target Language" }, "title": { @@ -1177,5 +1205,5 @@ ], "title": "Lesson", "type": "object", - "x-schema-version": "1.8" + "x-schema-version": "1.9" } diff --git a/schema/quality-rules.json b/schema/quality-rules.json index ef23cd5..c88a281 100644 --- a/schema/quality-rules.json +++ b/schema/quality-rules.json @@ -1,5 +1,5 @@ { - "$schema-version": "1.8", + "$schema-version": "1.9", "_comment": "GENERATED from scripts/generate_lesson_schema.py (EXP-039). Do not edit. Shared quality minimums for the content quality gate (app + content repo).", "rules": { "minExerciseTypes": 2, diff --git a/schema/stable-id-coverage.txt b/schema/stable-id-coverage.txt new file mode 100644 index 0000000..00750ed --- /dev/null +++ b/schema/stable-id-coverage.txt @@ -0,0 +1 @@ +3 diff --git a/scripts/check_stable_id_coverage.py b/scripts/check_stable_id_coverage.py new file mode 100644 index 0000000..1ee246d --- /dev/null +++ b/scripts/check_stable_id_coverage.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python3 +"""Coverage ratchet for stable_id (engine#90). + +The STABILITY half (does a published id still point at its element?) is not +here: it ships with the pinned engine as ``learn-content-engine +check-stable-ids`` and is called from ``make stable-ids``. Ten vendored +copies would drift; one shipped command does not. + +What stays repo-local is the COVERAGE number, because it is a property of +this repository: how many of its sets are FULLY minted (every exercise and +card in every lesson carries a stable_id; half a set is half a promise). +The committed baseline (``schema/stable-id-coverage.txt``) may only be +crossed deliberately: computed < baseline is a regression, computed > +baseline demands a conscious raise. Both are red. + +The run prints the checked quantities, and a repo whose manifest lists no +sets fails rather than reporting full coverage over nothing. + + python3 scripts/check_stable_id_coverage.py + python3 scripts/check_stable_id_coverage.py --self-test +""" +from __future__ import annotations + +import json +import sys +import tempfile +from pathlib import Path + +import yaml + +REPO_ROOT = Path(__file__).resolve().parents[1] +BASELINE = REPO_ROOT / "schema" / "stable-id-coverage.txt" + + +def set_fully_minted(set_dir: Path) -> bool: + manifest_path = set_dir / "manifest.yaml" + if not manifest_path.is_file(): + return False + set_manifest = yaml.safe_load(manifest_path.read_text(encoding="utf-8")) or {} + lesson_names = (set_manifest.get("metadata") or {}).get("lessons") or [] + if not lesson_names: + return False + for name in lesson_names: + lesson_path = set_dir / "lessons" / name + if not lesson_path.is_file(): + return False + lesson = json.loads(lesson_path.read_text(encoding="utf-8")) + for card in lesson.get("cards") or []: + if not card.get("stable_id"): + return False + for step in lesson.get("steps") or []: + exercise = step.get("exercise") + if exercise and not exercise.get("stable_id"): + return False + return True + + +def coverage(repo_root: Path) -> tuple[int, int]: + manifest = yaml.safe_load((repo_root / "manifest.yaml").read_text(encoding="utf-8")) or {} + root_sets = manifest.get("sets") or [] + covered = sum( + 1 for s in root_sets if s.get("path") and set_fully_minted(repo_root / s["path"]) + ) + return covered, len(root_sets) + + +def gate(repo_root: Path, baseline_path: Path) -> int: + covered, total = coverage(repo_root) + baseline = int(baseline_path.read_text(encoding="utf-8").strip()) if baseline_path.is_file() else 0 + print(f"stable-id coverage: {covered} of {total} set(s) fully minted, baseline {baseline}") + if total == 0: + print("FAIL: the root manifest lists no sets; a run over nothing is never fully covered") + return 1 + if covered < baseline: + print(f"FAIL: coverage {covered} below baseline {baseline} (regression)") + return 1 + if covered > baseline: + print(f"FAIL: coverage {covered} above baseline {baseline}; raise it deliberately in {baseline_path.name}") + return 1 + print("OK: coverage equals the baseline") + return 0 + + +def self_test() -> int: + """Prove the three red paths fire; a ratchet nobody saw trip is decoration.""" + failures = [] + lesson = { + "id": "l1", + "title": "L1", + "cards": [{"id": "c1", "front": "f", "back": "b", "stable_id": "card-selftest01"}], + "steps": [ + { + "id": "s1", + "type": "exercise", + "exercise": { + "id": "e1", + "type": "free_text", + "prompt": "p", + "accept": ["a"], + "stable_id": "ex-selftest001", + }, + } + ], + } + + def build(tmp: Path, minted: bool, with_sets: bool = True) -> None: + (tmp / "schema").mkdir(parents=True, exist_ok=True) + if not with_sets: + (tmp / "manifest.yaml").write_text("name: Leer\nsets: []\n", encoding="utf-8") + return + lessons = tmp / "sets/de/demo/lessons" + lessons.mkdir(parents=True, exist_ok=True) + payload = json.loads(json.dumps(lesson)) + if not minted: + del payload["cards"][0]["stable_id"] + del payload["steps"][0]["exercise"]["stable_id"] + (lessons / "01-demo.json").write_text(json.dumps(payload), encoding="utf-8") + (tmp / "sets/de/demo/manifest.yaml").write_text( + "name: Demo\nsets:\n - id: demo\nmetadata:\n lessons:\n - 01-demo.json\n", + encoding="utf-8", + ) + (tmp / "manifest.yaml").write_text( + "name: Demo\nsets:\n - id: demo\n path: sets/de/demo\n", encoding="utf-8" + ) + + scenarios = [ + ("regression (minted 0, baseline 1)", False, True, "1", "below baseline"), + ("undeclared raise (minted 1, baseline 0)", True, True, "0", "above baseline"), + ("no sets at all", True, False, "0", "lists no sets"), + ] + for name, minted, with_sets, baseline_value, marker in scenarios: + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + build(tmp_path, minted, with_sets) + baseline_path = tmp_path / "schema" / "stable-id-coverage.txt" + baseline_path.write_text(f"{baseline_value}\n", encoding="utf-8") + import io + import contextlib + + buffer = io.StringIO() + with contextlib.redirect_stdout(buffer): + code = gate(tmp_path, baseline_path) + if code == 0 or marker not in buffer.getvalue(): + failures.append(f"{name}: did not fire (exit={code})\n{buffer.getvalue()}") + else: + print(f"self-test OK: {name}") + + # Positive control: a matching baseline must PASS, or the gate is just + # always-red and proves nothing. + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + build(tmp_path, True, True) + baseline_path = tmp_path / "schema" / "stable-id-coverage.txt" + baseline_path.write_text("1\n", encoding="utf-8") + if gate(tmp_path, baseline_path) != 0: + failures.append("positive control: a matching baseline was not accepted") + else: + print("self-test OK: positive control (baseline matches)") + + if failures: + print("SELF-TEST FAIL:") + for failure in failures: + print(failure) + return 1 + print("Self-test passed: every ratchet path fires, and a matching baseline passes.") + return 0 + + +if __name__ == "__main__": + sys.exit(self_test() if "--self-test" in sys.argv else gate(REPO_ROOT, BASELINE)) diff --git a/sets/de/python-basics/lessons/01-erste-schritte.json b/sets/de/python-basics/lessons/01-erste-schritte.json index 1646add..c2ea0f1 100644 --- a/sets/de/python-basics/lessons/01-erste-schritte.json +++ b/sets/de/python-basics/lessons/01-erste-schritte.json @@ -9,6 +9,7 @@ "cards": [ { "id": "py-was-ist-python", + "stable_id": "card-ms942h7d001ly", "front": "Python", "back": "Eine beliebte, gut lesbare Programmiersprache für Einsteiger und Profis.", "notes": "Benannt nach der Komikertruppe Monty Python, nicht nach der Schlange.", @@ -18,6 +19,7 @@ }, { "id": "py-interpreter", + "stable_id": "card-ms942h7d0024y", "front": "Interpreter", "back": "Programm, das Python-Code Zeile für Zeile liest und sofort ausführt.", "notes": "Deshalb braucht man bei Python keinen separaten Kompilier-Schritt wie in C oder Java.", @@ -27,6 +29,7 @@ }, { "id": "py-print", + "stable_id": "card-ms942h7d0035p", "front": "print()", "back": "Gibt Text auf der Konsole aus.", "code_snippet": "print('Hallo Welt')", @@ -40,6 +43,7 @@ }, { "id": "py-hello-world", + "stable_id": "card-ms942h7d004oq", "front": "Hallo-Welt-Programm", "back": "Das traditionelle erste Programm in jeder Programmiersprache.", "code_snippet": "print('Hallo Welt')", @@ -53,6 +57,7 @@ }, { "id": "py-print-mehrere", + "stable_id": "card-ms942h7d005yp", "front": "print() mit mehreren Werten", "back": "print() trennt mehrere Argumente automatisch mit einem Leerzeichen.", "code_snippet": "print('Hallo', 'Welt')", @@ -66,6 +71,7 @@ }, { "id": "py-string", + "stable_id": "card-ms942h7d006vw", "front": "String (str)", "back": "Eine Zeichenkette — Text, der in Anführungszeichen steht.", "code_snippet": "name = 'Anna'\nprint(name)", @@ -79,6 +85,7 @@ }, { "id": "py-comment", + "stable_id": "card-ms942h7d007vc", "front": "Kommentar (#)", "back": "Text nach einem # wird vom Interpreter ignoriert — er dient nur dem Menschen.", "code_snippet": "# Das ist ein Kommentar\nprint('Hi')", @@ -92,6 +99,7 @@ }, { "id": "py-quotes", + "stable_id": "card-ms942h7d008h1", "front": "Anführungszeichen", "back": "Strings stehen in einfachen ' ' oder doppelten \" \" Anführungszeichen — Hauptsache, beide Seiten sind gleich.", "code_snippet": "print(\"Tschüss\")", @@ -105,6 +113,7 @@ }, { "id": "py-syntaxerror", + "stable_id": "card-ms942h7d0098l", "front": "SyntaxError", "back": "Fehlermeldung, wenn der Code grammatikalisch falsch ist, z. B. bei einer fehlenden Klammer.", "notes": "Python zeigt die Zeile mit dem Problem an — sehr hilfreich beim Suchen.", @@ -114,6 +123,7 @@ }, { "id": "py-einrueckung", + "stable_id": "card-ms942h7d00avb", "front": "Einrückung (Indentation)", "back": "Leerzeichen am Zeilenanfang, mit denen Python zusammengehörige Code-Blöcke strukturiert.", "notes": "Andere Sprachen nutzen { }, Python nutzt die Einrückung. Üblich sind 4 Leerzeichen.", @@ -148,6 +158,7 @@ "title": "Die Ausgabe-Funktion", "exercise": { "id": "ex-cloze-print", + "stable_id": "ex-ms942h7d00bxc", "type": "cloze", "prompt": "Ergänze den Namen der Funktion, die Text auf der Konsole ausgibt.", "card_ids": ["py-print"], @@ -166,6 +177,7 @@ "title": "Welche Funktion gibt aus?", "exercise": { "id": "ex-free-output", + "stable_id": "ex-ms942h7d00cxe", "type": "free_text", "prompt": "Welche Funktion gibt Text auf der Konsole aus?", "card_ids": ["py-print"], @@ -182,6 +194,7 @@ "title": "Das Hallo-Welt-Programm zusammensetzen", "exercise": { "id": "ex-tiles-hello", + "stable_id": "ex-ms942h7d00d19", "type": "word_tiles", "prompt": "Setze die Code-Zeile zusammen, die 'Hallo Welt' ausgibt.", "card_ids": ["py-hello-world"], @@ -197,6 +210,7 @@ "title": "Begriffe zuordnen", "exercise": { "id": "ex-match-begriffe", + "stable_id": "ex-ms942h7d00efn", "type": "matching", "prompt": "Ordne jeden Begriff seiner Beschreibung zu.", "card_ids": ["py-interpreter", "py-string", "py-comment", "py-print"], @@ -216,6 +230,7 @@ "title": "Kommentare einleiten", "exercise": { "id": "ex-cloze-comment", + "stable_id": "ex-ms942h7d00fgt", "type": "cloze", "prompt": "Ergänze das Zeichen, mit dem ein Kommentar beginnt.", "card_ids": ["py-comment"], @@ -234,6 +249,7 @@ "title": "Das Kommentarzeichen", "exercise": { "id": "ex-free-comment", + "stable_id": "ex-ms942h7d00gvf", "type": "free_text", "prompt": "Welches Zeichen leitet in Python einen Kommentar ein?", "card_ids": ["py-comment"], @@ -250,6 +266,7 @@ "title": "Zwei Wörter ausgeben", "exercise": { "id": "ex-tiles-print-mehrere", + "stable_id": "ex-ms942h7d00hx8", "type": "word_tiles", "prompt": "Setze die Zeile zusammen, die 'Hallo' und 'Welt' als zwei Argumente ausgibt.", "card_ids": ["py-print-mehrere"], diff --git a/sets/de/python-basics/lessons/02-variablen-und-datentypen.json b/sets/de/python-basics/lessons/02-variablen-und-datentypen.json index 7753642..c45726e 100644 --- a/sets/de/python-basics/lessons/02-variablen-und-datentypen.json +++ b/sets/de/python-basics/lessons/02-variablen-und-datentypen.json @@ -9,6 +9,7 @@ "cards": [ { "id": "py-variable", + "stable_id": "card-ms942h7f00inq", "front": "Variable", "back": "Ein benannter Speicherplatz, der einen Wert enthält. Mit = wird ihr ein Wert zugewiesen.", "notes": "Der Name einer Variablen ist frei wählbar — solange er den Namensregeln folgt.", @@ -18,6 +19,7 @@ }, { "id": "py-zuweisung", + "stable_id": "card-ms942h7f00jp9", "front": "Zuweisung (=)", "back": "Der Operator = weist einer Variablen einen Wert zu: name = 'Anna' speichert den String 'Anna'.", "code_snippet": "name = 'Anna'\nprint(name)", @@ -30,6 +32,7 @@ }, { "id": "py-int", + "stable_id": "card-ms942h7f00klb", "front": "int (Ganzzahl)", "back": "Datentyp für ganze Zahlen ohne Nachkommastellen, z. B. 42 oder -7.", "code_snippet": "alter = 25\nprint(alter)", @@ -42,6 +45,7 @@ }, { "id": "py-float", + "stable_id": "card-ms942h7f00lrv", "front": "float (Kommazahl)", "back": "Datentyp für Zahlen mit Nachkommastellen, z. B. 3.14 oder -0.5. Der Dezimaltrenner ist ein Punkt.", "code_snippet": "preis = 9.99\nprint(preis)", @@ -54,6 +58,7 @@ }, { "id": "py-str-typ", + "stable_id": "card-ms942h7f00m1q", "front": "str (Zeichenkette)", "back": "Datentyp für Text. Strings stehen in einfachen oder doppelten Anführungszeichen.", "code_snippet": "begruessung = 'Hallo'\nprint(begruessung)", @@ -66,6 +71,7 @@ }, { "id": "py-bool", + "stable_id": "card-ms942h7f00ns9", "front": "bool (Wahrheitswert)", "back": "Datentyp mit genau zwei Werten: True (wahr) oder False (falsch).", "code_snippet": "ist_angemeldet = True\nprint(ist_angemeldet)", @@ -78,6 +84,7 @@ }, { "id": "py-type-funktion", + "stable_id": "card-ms942h7f00o0p", "front": "type()", "back": "Gibt den Datentyp eines Wertes zurück, z. B. oder .", "code_snippet": "print(type(42))\nprint(type('Hallo'))", @@ -90,6 +97,7 @@ }, { "id": "py-namensregeln", + "stable_id": "card-ms942h7f00p8r", "front": "Namensregeln für Variablen", "back": "Variablennamen bestehen aus Buchstaben, Ziffern und _ . Sie dürfen nicht mit einer Ziffer beginnen und keine Leerzeichen enthalten.", "notes": "Python unterscheidet Groß- und Kleinschreibung: alter und Alter sind zwei verschiedene Variablen.", @@ -99,6 +107,7 @@ }, { "id": "py-ueberschreiben", + "stable_id": "card-ms942h7f00qhd", "front": "Wert überschreiben", "back": "Man kann einer Variablen jederzeit einen neuen Wert zuweisen. Der alte Wert wird dabei ersetzt.", "code_snippet": "x = 10\nx = 20\nprint(x)", @@ -111,6 +120,7 @@ }, { "id": "py-snake-case", + "stable_id": "card-ms942h7f00r94", "front": "Snake Case", "back": "Schreibweise für mehrteilige Variablennamen in Python: Wörter werden mit Unterstrichen verbunden, alles klein geschrieben.", "notes": "Beispiel: mein_name, hoechste_punktzahl. Python-Stil bevorzugt snake_case gegenüber camelCase.", @@ -120,6 +130,7 @@ }, { "id": "py-mehrere-typen", + "stable_id": "card-ms942h7f00st8", "front": "Vier Grundtypen im Überblick", "back": "int = ganze Zahl, float = Kommazahl, str = Text, bool = True/False.", "code_snippet": "print(type(5))\nprint(type(3.14))\nprint(type('Hi'))\nprint(type(False))", @@ -157,6 +168,7 @@ "title": "Zuweisung ergänzen", "exercise": { "id": "ex-cloze-zuweisung", + "stable_id": "ex-ms942h7f00tcl", "type": "cloze", "prompt": "Ergänze den Operator, mit dem man in Python einer Variablen einen Wert zuweist.", "card_ids": ["py-zuweisung"], @@ -175,6 +187,7 @@ "title": "type() kennenlernen", "exercise": { "id": "ex-cloze-type", + "stable_id": "ex-ms942h7f00ube", "type": "cloze", "prompt": "Welche Funktion gibt den Datentyp eines Wertes zurück? Ergänze den Namen.", "card_ids": ["py-type-funktion"], @@ -193,6 +206,7 @@ "title": "Datentyp ganzer Zahlen", "exercise": { "id": "ex-free-int", + "stable_id": "ex-ms942h7f00vca", "type": "free_text", "prompt": "Wie heißt der Python-Datentyp für ganze Zahlen wie 42 oder -7?", "card_ids": ["py-int"], @@ -209,6 +223,7 @@ "title": "Wahrheitswerte", "exercise": { "id": "ex-free-bool", + "stable_id": "ex-ms942h7f00wvu", "type": "free_text", "prompt": "Welche zwei Werte kann eine Variable vom Typ bool annehmen?", "card_ids": ["py-bool"], @@ -225,6 +240,7 @@ "title": "Variable anlegen", "exercise": { "id": "ex-tiles-variable", + "stable_id": "ex-ms942h7f00xom", "type": "word_tiles", "prompt": "Setze die Zeile zusammen, die der Variablen 'alter' den Wert 25 zuweist.", "card_ids": ["py-int"], @@ -240,6 +256,7 @@ "title": "Datentypen zuordnen", "exercise": { "id": "ex-match-datentypen", + "stable_id": "ex-ms942h7f00yyg", "type": "matching", "prompt": "Ordne jeden Wert seinem Datentyp zu.", "card_ids": ["py-int", "py-float", "py-str-typ", "py-bool"], @@ -259,6 +276,7 @@ "title": "Namensregeln prüfen", "exercise": { "id": "ex-match-regeln", + "stable_id": "ex-ms942h7f00z23", "type": "matching", "prompt": "Ordne jeden Variablennamen dem richtigen Urteil zu.", "card_ids": ["py-namensregeln", "py-snake-case"], diff --git a/sets/de/python-basics/lessons/03-zahlen-und-operatoren.json b/sets/de/python-basics/lessons/03-zahlen-und-operatoren.json index 0c30400..e0a3d9b 100644 --- a/sets/de/python-basics/lessons/03-zahlen-und-operatoren.json +++ b/sets/de/python-basics/lessons/03-zahlen-und-operatoren.json @@ -9,6 +9,7 @@ "cards": [ { "id": "py-addition", + "stable_id": "card-ms942h7g0109b", "front": "Addition (+)", "back": "Addiert zwei Zahlen. Funktioniert mit int und float.", "code_snippet": "print(3 + 5)\nprint(1.5 + 2.5)", @@ -21,6 +22,7 @@ }, { "id": "py-subtraktion", + "stable_id": "card-ms942h7g011fc", "front": "Subtraktion (-)", "back": "Zieht den rechten Wert vom linken ab.", "code_snippet": "print(10 - 3)", @@ -33,6 +35,7 @@ }, { "id": "py-multiplikation", + "stable_id": "card-ms942h7g012rx", "front": "Multiplikation (*)", "back": "Multipliziert zwei Zahlen. In Python schreibt man * statt ×.", "code_snippet": "print(4 * 7)", @@ -45,6 +48,7 @@ }, { "id": "py-division", + "stable_id": "card-ms942h7g0138a", "front": "Division (/)", "back": "Dividiert zwei Zahlen. Das Ergebnis ist immer ein float, auch wenn beide Zahlen int sind.", "code_snippet": "print(10 / 4)\nprint(8 / 2)", @@ -57,6 +61,7 @@ }, { "id": "py-ganzzahldivision", + "stable_id": "card-ms942h7g014td", "front": "Ganzzahldivision (//)", "back": "Dividiert und schneidet die Nachkommastellen ab. Das Ergebnis ist ein int (wenn beide Operanden int sind).", "code_snippet": "print(10 // 3)\nprint(7 // 2)", @@ -69,6 +74,7 @@ }, { "id": "py-modulo", + "stable_id": "card-ms942h7g0151n", "front": "Modulo (%)", "back": "Gibt den Rest einer ganzzahligen Division zurück. Nützlich z. B. um zu prüfen, ob eine Zahl gerade ist.", "code_snippet": "print(10 % 3)\nprint(8 % 2)", @@ -81,6 +87,7 @@ }, { "id": "py-potenz", + "stable_id": "card-ms942h7g016gb", "front": "Potenz (**)", "back": "Berechnet eine Potenz: a ** b bedeutet a hoch b.", "code_snippet": "print(2 ** 8)\nprint(3 ** 2)", @@ -93,6 +100,7 @@ }, { "id": "py-rangfolge", + "stable_id": "card-ms942h7g017ai", "front": "Operatorrangfolge (Precedence)", "back": "Python rechnet nach der Regel Punkt vor Strich: erst **, dann * / // %, dann + -. Klammern ändern die Reihenfolge.", "code_snippet": "print(2 + 3 * 4)\nprint((2 + 3) * 4)", @@ -105,6 +113,7 @@ }, { "id": "py-int-float-division", + "stable_id": "card-ms942h7g018v0", "front": "int vs. float bei Division", "back": "Mit / erhält man immer einen float. Mit // erhält man einen int (bei int-Operanden), ohne Nachkommastellen.", "code_snippet": "print(type(8 / 2))\nprint(type(8 // 2))", @@ -117,6 +126,7 @@ }, { "id": "py-klammern", + "stable_id": "card-ms942h7g019gy", "front": "Klammern in Rechenausdrücken", "back": "Runde Klammern () erzwingen eine andere Berechnungsreihenfolge und machen den Code lesbarer.", "notes": "Klammern sind in Python auch für Funktionsaufrufe zuständig — der Kontext macht den Unterschied.", @@ -126,6 +136,7 @@ }, { "id": "py-operatoren-uebersicht", + "stable_id": "card-ms942h7g01afe", "front": "Arithmetische Operatoren im Überblick", "back": "+ Addition, - Subtraktion, * Multiplikation, / Division (float), // Ganzzahldivision, % Modulo, ** Potenz.", "media_type": "text", @@ -159,6 +170,7 @@ "title": "Divisionsoperator ergänzen", "exercise": { "id": "ex-cloze-division", + "stable_id": "ex-ms942h7g01bnf", "type": "cloze", "prompt": "Ergänze den Operator für die normale Division, die immer einen float zurückgibt.", "card_ids": ["py-division"], @@ -177,6 +189,7 @@ "title": "Modulo-Operator", "exercise": { "id": "ex-cloze-modulo", + "stable_id": "ex-ms942h7g01com", "type": "cloze", "prompt": "Ergänze den Operator, der den Rest einer Division zurückgibt.", "card_ids": ["py-modulo"], @@ -195,6 +208,7 @@ "title": "Potenz-Operator", "exercise": { "id": "ex-free-potenz", + "stable_id": "ex-ms942h7g01dss", "type": "free_text", "prompt": "Welches Ergebnis liefert 2 ** 8 in Python?", "card_ids": ["py-potenz"], @@ -211,6 +225,7 @@ "title": "Operatorrangfolge", "exercise": { "id": "ex-free-rangfolge", + "stable_id": "ex-ms942h7g01edg", "type": "free_text", "prompt": "Was gibt print(2 + 3 * 4) aus?", "card_ids": ["py-rangfolge"], @@ -227,6 +242,7 @@ "title": "Ganzzahldivision zusammensetzen", "exercise": { "id": "ex-tiles-ganzzahldivision", + "stable_id": "ex-ms942h7g01fve", "type": "word_tiles", "prompt": "Setze die Zeile zusammen, die die Ganzzahldivision von 10 durch 3 ausgibt.", "card_ids": ["py-ganzzahldivision"], @@ -242,6 +258,7 @@ "title": "Operatoren zuordnen", "exercise": { "id": "ex-match-operatoren", + "stable_id": "ex-ms942h7g01gw6", "type": "matching", "prompt": "Ordne jeden Operator seiner Bedeutung zu.", "card_ids": ["py-ganzzahldivision", "py-modulo", "py-potenz"], @@ -260,6 +277,7 @@ "title": "Rechenergebnisse zuordnen", "exercise": { "id": "ex-match-ergebnisse", + "stable_id": "ex-ms942h7g01h3d", "type": "matching", "prompt": "Ordne jeden Ausdruck dem richtigen Ergebnis zu.", "card_ids": ["py-addition", "py-multiplikation", "py-ganzzahldivision", "py-modulo"], diff --git a/sets/de/python-basics/lessons/04-strings.json b/sets/de/python-basics/lessons/04-strings.json index f4be82a..d4d6d7f 100644 --- a/sets/de/python-basics/lessons/04-strings.json +++ b/sets/de/python-basics/lessons/04-strings.json @@ -9,6 +9,7 @@ "cards": [ { "id": "py-str-verkettung", + "stable_id": "card-ms942h7g01i8n", "front": "String-Verkettung (+)", "back": "Zwei Strings lassen sich mit + zu einem neuen String zusammenfügen.", "code_snippet": "vorname = 'Anna'\nnachname = 'Müller'\nprint(vorname + ' ' + nachname)", @@ -21,6 +22,7 @@ }, { "id": "py-fstring", + "stable_id": "card-ms942h7g01jms", "front": "f-String (f\"...{variable}...\")", "back": "Ein formatierter String, bei dem Variablen direkt in geschweifte Klammern eingesetzt werden. Eingeleitet durch f vor dem Anführungszeichen.", "code_snippet": "name = 'Lena'\nalter = 22\nprint(f'Ich heiße {name} und bin {alter} Jahre alt.')", @@ -33,6 +35,7 @@ }, { "id": "py-len", + "stable_id": "card-ms942h7g01kao", "front": "len()", "back": "Gibt die Länge (Anzahl der Zeichen) eines Strings zurück.", "code_snippet": "wort = 'Python'\nprint(len(wort))", @@ -45,6 +48,7 @@ }, { "id": "py-upper-lower", + "stable_id": "card-ms942h7g01ltj", "front": ".upper() / .lower()", "back": ".upper() wandelt alle Buchstaben in Großbuchstaben um, .lower() in Kleinbuchstaben.", "code_snippet": "s = 'Hallo Welt'\nprint(s.upper())\nprint(s.lower())", @@ -57,6 +61,7 @@ }, { "id": "py-replace", + "stable_id": "card-ms942h7g01mdi", "front": ".replace(alt, neu)", "back": "Ersetzt alle Vorkommen eines Teilstrings durch einen anderen und gibt den neuen String zurück.", "code_snippet": "satz = 'Ich mag Katzen'\nprint(satz.replace('Katzen', 'Hunde'))", @@ -69,6 +74,7 @@ }, { "id": "py-indexing", + "stable_id": "card-ms942h7g01nt6", "front": "String-Indexing (s[i])", "back": "Mit eckigen Klammern und einem Index greift man auf ein einzelnes Zeichen zu. Der Index beginnt bei 0.", "code_snippet": "s = 'Python'\nprint(s[0])\nprint(s[3])", @@ -81,6 +87,7 @@ }, { "id": "py-slicing", + "stable_id": "card-ms942h7g01ooc", "front": "String-Slicing (s[start:stop])", "back": "Gibt einen Teilstring zurück. s[1:4] liefert die Zeichen an den Positionen 1, 2 und 3 (stop ist nicht enthalten).", "code_snippet": "s = 'Python'\nprint(s[0:3])\nprint(s[2:5])", @@ -93,6 +100,7 @@ }, { "id": "py-in-operator", + "stable_id": "card-ms942h7g01pok", "front": "in-Operator bei Strings", "back": "Mit in prüft man, ob ein Teilstring in einem anderen String enthalten ist. Das Ergebnis ist True oder False.", "code_snippet": "print('Py' in 'Python')\nprint('Java' in 'Python')", @@ -105,6 +113,7 @@ }, { "id": "py-str-unveraenderlich", + "stable_id": "card-ms942h7g01q83", "front": "Strings sind unveränderlich (immutable)", "back": "Strings können nach ihrer Erstellung nicht geändert werden. Methoden wie .upper() geben immer einen neuen String zurück.", "notes": "Das ist ein wichtiger Unterschied zu Listen, die veränderbar (mutable) sind.", @@ -114,6 +123,7 @@ }, { "id": "py-str-index-null", + "stable_id": "card-ms942h7g01r1b", "front": "Index 0 beim ersten Zeichen", "back": "Python zählt Zeichen in einem String ab 0. Das erste Zeichen ist s[0], das zweite s[1] usw.", "notes": "Diese Zählweise gilt auch für Listen und andere Sequenzen in Python.", @@ -123,6 +133,7 @@ }, { "id": "py-str-methoden-uebersicht", + "stable_id": "card-ms942h7g01sgm", "front": "Wichtige String-Methoden", "back": ".upper() → Großbuchstaben, .lower() → Kleinbuchstaben, .replace(alt, neu) → ersetzen, len(s) → Länge.", "media_type": "text", @@ -156,6 +167,7 @@ "title": "f-String-Präfix", "exercise": { "id": "ex-cloze-fstring", + "stable_id": "ex-ms942h7g01tpg", "type": "cloze", "prompt": "Ergänze den Buchstaben, der vor dem Anführungszeichen eines f-Strings steht.", "card_ids": ["py-fstring"], @@ -175,6 +187,7 @@ "title": "Länge eines Strings ermitteln", "exercise": { "id": "ex-cloze-len", + "stable_id": "ex-ms942h7g01ub6", "type": "cloze", "prompt": "Ergänze die Funktion, die die Anzahl der Zeichen in einem String zurückgibt.", "card_ids": ["py-len"], @@ -193,6 +206,7 @@ "title": "Indexing: erstes Zeichen", "exercise": { "id": "ex-free-indexing", + "stable_id": "ex-ms942h7g01vt4", "type": "free_text", "prompt": "Welchen Index hat das erste Zeichen eines Strings in Python?", "card_ids": ["py-indexing", "py-str-index-null"], @@ -209,6 +223,7 @@ "title": "Der in-Operator", "exercise": { "id": "ex-free-in-operator", + "stable_id": "ex-ms942h7g01w6a", "type": "free_text", "prompt": "Was gibt 'Py' in 'Python' zurück?", "card_ids": ["py-in-operator"], @@ -225,6 +240,7 @@ "title": "upper()-Aufruf zusammensetzen", "exercise": { "id": "ex-tiles-upper", + "stable_id": "ex-ms942h7g01xxt", "type": "word_tiles", "prompt": "Setze die Zeile zusammen, die den String 'hallo' in Großbuchstaben ausgibt.", "card_ids": ["py-upper-lower"], @@ -240,6 +256,7 @@ "title": "String-Methoden zuordnen", "exercise": { "id": "ex-match-methoden", + "stable_id": "ex-ms942h7g01ye3", "type": "matching", "prompt": "Ordne jede Methode oder Funktion ihrer Wirkung zu.", "card_ids": ["py-upper-lower", "py-replace", "py-len"], @@ -259,6 +276,7 @@ "title": "String-Konzepte zuordnen", "exercise": { "id": "ex-match-konzepte", + "stable_id": "ex-ms942h7g01zi8", "type": "matching", "prompt": "Ordne jeden Begriff seinem Beispiel zu.", "card_ids": ["py-str-verkettung", "py-fstring", "py-slicing", "py-in-operator"], diff --git a/sets/de/python-basics/lessons/05-listen.json b/sets/de/python-basics/lessons/05-listen.json index 5e0d5f5..8b310f6 100644 --- a/sets/de/python-basics/lessons/05-listen.json +++ b/sets/de/python-basics/lessons/05-listen.json @@ -9,6 +9,7 @@ "cards": [ { "id": "py-list-erstellen", + "stable_id": "card-ms942h7h020po", "front": "Liste erstellen (List)", "back": "Eine Liste speichert mehrere Werte in eckigen Klammern, getrennt durch Kommas.", "code_snippet": "farben = ['rot', 'gruen', 'blau']\nprint(farben)", @@ -21,6 +22,7 @@ }, { "id": "py-list-index", + "stable_id": "card-ms942h7h021xo", "front": "Indexing — liste[0]", "back": "Mit einem Index in eckigen Klammern greift man auf ein einzelnes Element zu. Der erste Index ist 0.", "code_snippet": "farben = ['rot', 'gruen', 'blau']\nprint(farben[0])", @@ -33,6 +35,7 @@ }, { "id": "py-list-neg-index", + "stable_id": "card-ms942h7h022v8", "front": "Negativer Index — liste[-1]", "back": "Negative Indizes zählen vom Ende der Liste. -1 ist das letzte Element.", "code_snippet": "farben = ['rot', 'gruen', 'blau']\nprint(farben[-1])", @@ -45,6 +48,7 @@ }, { "id": "py-list-slicing", + "stable_id": "card-ms942h7h023j8", "front": "Slicing — liste[1:3]", "back": "Mit Slicing schneidet man einen Teil einer Liste heraus. liste[start:stop] gibt Elemente von Index start bis stop-1 zurück.", "code_snippet": "zahlen = [10, 20, 30, 40, 50]\nprint(zahlen[1:3])", @@ -57,6 +61,7 @@ }, { "id": "py-list-len", + "stable_id": "card-ms942h7h02474", "front": "len() — Länge einer Liste", "back": "len(liste) gibt die Anzahl der Elemente in der Liste zurück.", "code_snippet": "farben = ['rot', 'gruen', 'blau']\nprint(len(farben))", @@ -69,6 +74,7 @@ }, { "id": "py-list-aendern", + "stable_id": "card-ms942h7h025pv", "front": "Element ändern", "back": "Mit liste[index] = neuer_wert ersetzt man ein Element an einem bestimmten Index.", "code_snippet": "farben = ['rot', 'gruen', 'blau']\nfarben[1] = 'gelb'\nprint(farben)", @@ -81,6 +87,7 @@ }, { "id": "py-list-append", + "stable_id": "card-ms942h7h026qx", "front": ".append() — Element hinzufügen", "back": ".append(element) fügt ein Element ans Ende der Liste an.", "code_snippet": "farben = ['rot', 'gruen']\nfarben.append('blau')\nprint(farben)", @@ -93,6 +100,7 @@ }, { "id": "py-list-remove", + "stable_id": "card-ms942h7h027mg", "front": ".remove() — Element entfernen", "back": ".remove(wert) entfernt das erste Vorkommen des angegebenen Werts aus der Liste.", "code_snippet": "farben = ['rot', 'gruen', 'blau']\nfarben.remove('gruen')\nprint(farben)", @@ -105,6 +113,7 @@ }, { "id": "py-list-insert", + "stable_id": "card-ms942h7h028g9", "front": ".insert() — Element einfügen", "back": ".insert(index, wert) fügt ein Element an einer bestimmten Position ein.", "code_snippet": "farben = ['rot', 'blau']\nfarben.insert(1, 'gruen')\nprint(farben)", @@ -117,6 +126,7 @@ }, { "id": "py-list-in", + "stable_id": "card-ms942h7h029sg", "front": "in-Test — Element prüfen", "back": "Mit dem Operator in prüft man, ob ein Wert in einer Liste enthalten ist. Das Ergebnis ist True oder False.", "code_snippet": "farben = ['rot', 'gruen', 'blau']\nprint('gruen' in farben)", @@ -129,6 +139,7 @@ }, { "id": "py-list-leer", + "stable_id": "card-ms942h7h02a9q", "front": "Leere Liste", "back": "Eine leere Liste wird mit [] erzeugt. Man kann später Elemente mit .append() hinzufügen.", "code_snippet": "meine_liste = []\nmeine_liste.append(42)\nprint(meine_liste)", @@ -160,6 +171,7 @@ "title": "Den ersten Index kennen", "exercise": { "id": "ex-cloze-index", + "stable_id": "ex-ms942h7h02bf3", "type": "cloze", "prompt": "Ergänze den Index, um das erste Element einer Liste abzurufen.", "card_ids": ["py-list-index"], @@ -178,6 +190,7 @@ "title": "Länge einer Liste bestimmen", "exercise": { "id": "ex-cloze-len", + "stable_id": "ex-ms942h7h02cxq", "type": "cloze", "prompt": "Ergänze die Funktion, die die Anzahl der Listenelemente zurückgibt.", "card_ids": ["py-list-len"], @@ -196,6 +209,7 @@ "title": "Element anhängen", "exercise": { "id": "ex-free-append", + "stable_id": "ex-ms942h7h02dkr", "type": "free_text", "prompt": "Welche Methode fügt ein Element ans Ende einer Liste an?", "card_ids": ["py-list-append"], @@ -212,6 +226,7 @@ "title": "Negativer Index", "exercise": { "id": "ex-free-neg-index", + "stable_id": "ex-ms942h7h02ec8", "type": "free_text", "prompt": "Welchen Index verwendet man, um das letzte Element einer Liste abzurufen?", "card_ids": ["py-list-neg-index"], @@ -228,6 +243,7 @@ "title": "append-Aufruf zusammensetzen", "exercise": { "id": "ex-tiles-append", + "stable_id": "ex-ms942h7h02f25", "type": "word_tiles", "prompt": "Setze den Code zusammen, der 'blau' an die Liste 'farben' anhängt.", "card_ids": ["py-list-append"], @@ -243,6 +259,7 @@ "title": "Listenmethoden zuordnen", "exercise": { "id": "ex-match-methoden", + "stable_id": "ex-ms942h7h02g11", "type": "matching", "prompt": "Ordne jede Listenmethode ihrer Funktion zu.", "card_ids": ["py-list-append", "py-list-remove", "py-list-insert", "py-list-len"], @@ -262,6 +279,7 @@ "title": "Letztes Element abrufen", "exercise": { "id": "ex-tiles-index", + "stable_id": "ex-ms942h7h02hmz", "type": "word_tiles", "prompt": "Setze den Ausdruck zusammen, der das letzte Element der Liste 'farben' ausgibt.", "card_ids": ["py-list-neg-index"], @@ -277,6 +295,7 @@ "title": "in-Operator", "exercise": { "id": "ex-cloze-in", + "stable_id": "ex-ms942h7h02ixg", "type": "cloze", "prompt": "Ergänze den Operator, um zu prüfen, ob ein Element in einer Liste vorhanden ist.", "card_ids": ["py-list-in"], diff --git a/sets/de/python-basics/lessons/06-bedingungen.json b/sets/de/python-basics/lessons/06-bedingungen.json index a91814a..bb71867 100644 --- a/sets/de/python-basics/lessons/06-bedingungen.json +++ b/sets/de/python-basics/lessons/06-bedingungen.json @@ -9,6 +9,7 @@ "cards": [ { "id": "py-if", + "stable_id": "card-ms942h7i02jnr", "front": "if-Anweisung", "back": "Führt einen Codeblock nur aus, wenn eine Bedingung wahr (True) ist.", "code_snippet": "alter = 18\nif alter >= 18:\n print('Volljährig')", @@ -21,6 +22,7 @@ }, { "id": "py-elif", + "stable_id": "card-ms942h7i02k5z", "front": "elif-Zweig", "back": "Steht nach einem if und prüft eine weitere Bedingung, wenn das vorherige if falsch war.", "code_snippet": "note = 2\nif note == 1:\n print('Sehr gut')\nelif note == 2:\n print('Gut')", @@ -33,6 +35,7 @@ }, { "id": "py-else", + "stable_id": "card-ms942h7i02lmw", "front": "else-Zweig", "back": "Wird ausgeführt, wenn keine der vorherigen if- oder elif-Bedingungen wahr war.", "code_snippet": "x = 5\nif x > 10:\n print('groß')\nelse:\n print('klein')", @@ -45,6 +48,7 @@ }, { "id": "py-vergleich-gleich", + "stable_id": "card-ms942h7i02mgi", "front": "== (Gleichheit)", "back": "Vergleichsoperator: prüft, ob zwei Werte gleich sind. Ergibt True oder False.", "code_snippet": "print(3 == 3)\nprint(3 == 4)", @@ -57,6 +61,7 @@ }, { "id": "py-vergleich-ungleich", + "stable_id": "card-ms942h7i02num", "front": "!= (Ungleichheit)", "back": "Vergleichsoperator: prüft, ob zwei Werte ungleich sind.", "code_snippet": "print(3 != 4)", @@ -69,6 +74,7 @@ }, { "id": "py-vergleich-kleiner-groesser", + "stable_id": "card-ms942h7i02o3z", "front": "< > <= >= (Größenvergleich)", "back": "Vergleichsoperatoren: < (kleiner), > (größer), <= (kleiner oder gleich), >= (größer oder gleich).", "code_snippet": "print(5 < 10)\nprint(10 >= 10)", @@ -81,6 +87,7 @@ }, { "id": "py-and", + "stable_id": "card-ms942h7i02p5e", "front": "and (logisches Und)", "back": "Boolescher Operator: beide Bedingungen müssen True sein, damit das Ergebnis True ist.", "code_snippet": "alter = 20\nnuechtern = True\nif alter >= 18 and nuechtern:\n print('Fahren erlaubt')", @@ -93,6 +100,7 @@ }, { "id": "py-or", + "stable_id": "card-ms942h7i02qqf", "front": "or (logisches Oder)", "back": "Boolescher Operator: mindestens eine der Bedingungen muss True sein.", "code_snippet": "tag = 'Samstag'\nif tag == 'Samstag' or tag == 'Sonntag':\n print('Wochenende')", @@ -105,6 +113,7 @@ }, { "id": "py-not", + "stable_id": "card-ms942h7i02rh6", "front": "not (logisches Nicht)", "back": "Boolescher Operator: kehrt einen Wahrheitswert um. not True ergibt False.", "code_snippet": "regen = False\nif not regen:\n print('Spaziergang möglich')", @@ -117,6 +126,7 @@ }, { "id": "py-einrueckung-block", + "stable_id": "card-ms942h7i02sef", "front": "Einrückung (Indentation) im if-Block", "back": "Der Code innerhalb eines if/elif/else-Blocks muss eingerückt sein (4 Leerzeichen). Die Einrückung zeigt Python, was zum Block gehört.", "notes": "Falsche oder fehlende Einrückung führt zu einem IndentationError.", @@ -126,6 +136,7 @@ }, { "id": "py-if-elif-else-kette", + "stable_id": "card-ms942h7i02tpa", "front": "if / elif / else — komplette Kette", "back": "Mit if, beliebig vielen elif und einem abschließenden else deckt man alle Fälle ab.", "code_snippet": "punkte = 75\nif punkte >= 90:\n print('Sehr gut')\nelif punkte >= 60:\n print('Bestanden')\nelse:\n print('Nicht bestanden')", @@ -138,6 +149,7 @@ }, { "id": "py-bool-ausdruck", + "stable_id": "card-ms942h7i02uex", "front": "Boolescher Ausdruck (Boolean Expression)", "back": "Ein Ausdruck, der True oder False ergibt — z. B. 5 > 3 oder alter >= 18.", "media_type": "text", @@ -171,6 +183,7 @@ "title": "if-Syntax ergänzen", "exercise": { "id": "ex-cloze-if-syntax", + "stable_id": "ex-ms942h7i02vb6", "type": "cloze", "prompt": "Ergänze das Zeichen, das nach der Bedingung eines if-Ausdrucks stehen muss.", "card_ids": ["py-if"], @@ -189,6 +202,7 @@ "title": "Gleichheitsoperator", "exercise": { "id": "ex-cloze-vergleich", + "stable_id": "ex-ms942h7i02w7d", "type": "cloze", "prompt": "Ergänze den Operator, der prüft, ob zwei Werte gleich sind.", "card_ids": ["py-vergleich-gleich"], @@ -207,6 +221,7 @@ "title": "Was macht elif?", "exercise": { "id": "ex-free-elif", + "stable_id": "ex-ms942h7i02x5w", "type": "free_text", "prompt": "Was bedeutet elif in Python?", "card_ids": ["py-elif"], @@ -223,6 +238,7 @@ "title": "and-Operator", "exercise": { "id": "ex-free-and", + "stable_id": "ex-ms942h7i02ymt", "type": "free_text", "prompt": "Wann ergibt eine Bedingung mit and den Wert True?", "card_ids": ["py-and"], @@ -239,6 +255,7 @@ "title": "if-Block zusammensetzen", "exercise": { "id": "ex-tiles-if-block", + "stable_id": "ex-ms942h7i02z15", "type": "word_tiles", "prompt": "Setze eine if-Anweisung zusammen, die prüft ob x größer als 0 ist.", "card_ids": ["py-if"], @@ -254,6 +271,7 @@ "title": "and-Bedingung zusammensetzen", "exercise": { "id": "ex-tiles-and", + "stable_id": "ex-ms942h7i0306d", "type": "word_tiles", "prompt": "Setze eine Bedingung zusammen, die prüft ob x größer als 0 UND kleiner als 10 ist.", "card_ids": ["py-and"], @@ -269,6 +287,7 @@ "title": "Vergleichsoperatoren zuordnen", "exercise": { "id": "ex-match-operatoren", + "stable_id": "ex-ms942h7i031zn", "type": "matching", "prompt": "Ordne jeden Operator seiner Bedeutung zu.", "card_ids": ["py-vergleich-gleich", "py-vergleich-ungleich", "py-vergleich-kleiner-groesser"], @@ -288,6 +307,7 @@ "title": "Boolesche Operatoren zuordnen", "exercise": { "id": "ex-match-bool-op", + "stable_id": "ex-ms942h7i032nm", "type": "matching", "prompt": "Ordne jeden booleschen Operator seiner Beschreibung zu.", "card_ids": ["py-and", "py-or", "py-not"], diff --git a/sets/de/python-basics/lessons/07-schleifen.json b/sets/de/python-basics/lessons/07-schleifen.json index 90280b0..b77b5a7 100644 --- a/sets/de/python-basics/lessons/07-schleifen.json +++ b/sets/de/python-basics/lessons/07-schleifen.json @@ -9,6 +9,7 @@ "cards": [ { "id": "py-for-liste", + "stable_id": "card-ms942h7i033i0", "front": "for-Schleife über eine Liste", "back": "Die for-Schleife wiederholt einen Codeblock für jedes Element einer Liste.", "code_snippet": "farben = ['rot', 'gruen', 'blau']\nfor farbe in farben:\n print(farbe)", @@ -21,6 +22,7 @@ }, { "id": "py-for-range", + "stable_id": "card-ms942h7i034dk", "front": "for-Schleife mit range()", "back": "range(n) erzeugt eine Folge von ganzen Zahlen von 0 bis n-1. Ideal zum Wiederholen einer festen Anzahl von Schritten.", "code_snippet": "for i in range(3):\n print(i)", @@ -33,6 +35,7 @@ }, { "id": "py-range-start-stop", + "stable_id": "card-ms942h7i0353o", "front": "range(start, stop)", "back": "range(start, stop) erzeugt Zahlen von start bis stop-1. Der stop-Wert selbst ist nicht enthalten.", "code_snippet": "for i in range(2, 5):\n print(i)", @@ -45,6 +48,7 @@ }, { "id": "py-while", + "stable_id": "card-ms942h7i036r3", "front": "while-Schleife", "back": "Die while-Schleife wiederholt einen Block, solange die Bedingung True ist.", "code_snippet": "zaehler = 0\nwhile zaehler < 3:\n print(zaehler)\n zaehler = zaehler + 1", @@ -57,6 +61,7 @@ }, { "id": "py-break", + "stable_id": "card-ms942h7i037tj", "front": "break — Schleife abbrechen", "back": "break beendet die Schleife sofort, egal ob die Bedingung noch True wäre.", "code_snippet": "for i in range(5):\n if i == 3:\n break\n print(i)", @@ -69,6 +74,7 @@ }, { "id": "py-continue", + "stable_id": "card-ms942h7i038ov", "front": "continue — Iteration überspringen", "back": "continue überspringt den Rest des aktuellen Schleifendurchlaufs und springt zum nächsten.", "code_snippet": "for i in range(5):\n if i == 2:\n continue\n print(i)", @@ -81,6 +87,7 @@ }, { "id": "py-zaehlen-schleife", + "stable_id": "card-ms942h7i039yn", "front": "Zählschleife — Summe berechnen", "back": "Ein typisches Muster: Variable vor der Schleife auf 0 setzen, in der Schleife aufaddieren.", "code_snippet": "summe = 0\nfor zahl in [1, 2, 3, 4, 5]:\n summe = summe + zahl\nprint(summe)", @@ -93,6 +100,7 @@ }, { "id": "py-schleife-einrueckung", + "stable_id": "card-ms942h7i03aa5", "front": "Einrückung in Schleifen", "back": "Der Körper einer Schleife (alles, was wiederholt werden soll) muss eingerückt sein — genau wie bei if-Blöcken.", "notes": "Ohne korrekte Einrückung gehört der Code nicht zum Schleifenkörper und wird nur einmal ausgeführt.", @@ -102,6 +110,7 @@ }, { "id": "py-for-vs-while", + "stable_id": "card-ms942h7i03buy", "front": "for vs. while — Wann was?", "back": "for: wenn man die Anzahl der Wiederholungen kennt oder über eine Sammlung iteriert. while: wenn man wiederholen will, bis eine Bedingung eintritt.", "media_type": "text", @@ -110,6 +119,7 @@ }, { "id": "py-range-step", + "stable_id": "card-ms942h7i03cvv", "front": "range(start, stop, step)", "back": "Mit einem dritten Argument legt man die Schrittweite fest. range(0, 10, 2) erzeugt 0, 2, 4, 6, 8.", "code_snippet": "for i in range(0, 6, 2):\n print(i)", @@ -147,6 +157,7 @@ "title": "range() verstehen", "exercise": { "id": "ex-cloze-range", + "stable_id": "ex-ms942h7i03dfl", "type": "cloze", "prompt": "Ergänze den Startpunkt von range().", "card_ids": ["py-for-range"], @@ -165,6 +176,7 @@ "title": "for-Syntax", "exercise": { "id": "ex-cloze-for-syntax", + "stable_id": "ex-ms942h7i03ew8", "type": "cloze", "prompt": "Ergänze das Schlüsselwort, das in einer for-Schleife die Sammlung einleitet.", "card_ids": ["py-for-liste"], @@ -183,6 +195,7 @@ "title": "Was macht break?", "exercise": { "id": "ex-free-break", + "stable_id": "ex-ms942h7i03f6q", "type": "free_text", "prompt": "Was bewirkt das Schlüsselwort break innerhalb einer Schleife?", "card_ids": ["py-break"], @@ -199,6 +212,7 @@ "title": "while-Schleife", "exercise": { "id": "ex-free-while", + "stable_id": "ex-ms942h7i03gf7", "type": "free_text", "prompt": "Wann hört eine while-Schleife auf zu laufen?", "card_ids": ["py-while"], @@ -215,6 +229,7 @@ "title": "for-range-Schleife zusammensetzen", "exercise": { "id": "ex-tiles-for-range", + "stable_id": "ex-ms942h7i03hgk", "type": "word_tiles", "prompt": "Setze eine for-Schleife zusammen, die 5 Mal von 0 bis 4 durchläuft.", "card_ids": ["py-for-range"], @@ -230,6 +245,7 @@ "title": "Summenvariable initialisieren", "exercise": { "id": "ex-tiles-summe", + "stable_id": "ex-ms942h7i03ij7", "type": "word_tiles", "prompt": "Setze die Zeile zusammen, die eine Summenvariable auf 0 setzt — bevor die Schleife startet.", "card_ids": ["py-zaehlen-schleife"], @@ -245,6 +261,7 @@ "title": "Schleifenbegriffe zuordnen", "exercise": { "id": "ex-match-schleifen", + "stable_id": "ex-ms942h7i03jpv", "type": "matching", "prompt": "Ordne jeden Begriff seiner Beschreibung zu.", "card_ids": ["py-for-range", "py-while", "py-break", "py-continue"], diff --git a/sets/de/python-basics/lessons/08-funktionen.json b/sets/de/python-basics/lessons/08-funktionen.json index 92d333a..eb5a356 100644 --- a/sets/de/python-basics/lessons/08-funktionen.json +++ b/sets/de/python-basics/lessons/08-funktionen.json @@ -9,6 +9,7 @@ "cards": [ { "id": "fn-def", + "stable_id": "card-ms942h7j03kau", "front": "def", "back": "Schlüsselwort, das eine Funktionsdefinition einleitet.", "code_snippet": "def begruesse():\n print('Hallo!')\n\nbegruesse()", @@ -22,6 +23,7 @@ }, { "id": "fn-parameter", + "stable_id": "card-ms942h7j03lki", "front": "Parameter", "back": "Platzhaltervariable im Funktionskopf, die beim Aufruf einen Wert erhält.", "code_snippet": "def begruesse(name):\n print('Hallo', name)\n\nbegruesse('Anna')", @@ -35,6 +37,7 @@ }, { "id": "fn-argument", + "stable_id": "card-ms942h7j03mrc", "front": "Argument", "back": "Der konkrete Wert, der beim Funktionsaufruf übergeben wird.", "notes": "Unterschied: Parameter ist der Name in der Definition; Argument ist der Wert beim Aufruf.", @@ -44,6 +47,7 @@ }, { "id": "fn-return", + "stable_id": "card-ms942h7j03ni8", "front": "return", "back": "Beendet eine Funktion und gibt einen Wert an den Aufrufer zurück.", "code_snippet": "def addiere(a, b):\n return a + b\n\nergebnis = addiere(3, 4)\nprint(ergebnis)", @@ -57,6 +61,7 @@ }, { "id": "fn-default-arg", + "stable_id": "card-ms942h7j03okp", "front": "Default-Argument (Standardwert)", "back": "Ein Parameter mit einem vordefinierten Wert, der verwendet wird, wenn beim Aufruf kein Argument übergeben wird.", "code_snippet": "def begruesse(name='Welt'):\n print('Hallo', name)\n\nbegruesse()\nbegruesse('Max')", @@ -69,6 +74,7 @@ }, { "id": "fn-aufruf", + "stable_id": "card-ms942h7j03pwi", "front": "Funktionsaufruf", "back": "Das Ausführen einer Funktion durch ihren Namen gefolgt von runden Klammern (ggf. mit Argumenten).", "code_snippet": "def quadrat(x):\n return x * x\n\nprint(quadrat(5))", @@ -81,6 +87,7 @@ }, { "id": "fn-docstring", + "stable_id": "card-ms942h7j03qjl", "front": "Docstring", "back": "Ein Dokumentations-String direkt nach dem def-Kopf, der in dreifachen Anführungszeichen steht und die Funktion beschreibt.", "code_snippet": "def berechne(x):\n \"\"\"Gibt das Doppelte von x zurueck.\"\"\"\n return x * 2\n\nprint(berechne(6))", @@ -94,6 +101,7 @@ }, { "id": "fn-print-vs-return", + "stable_id": "card-ms942h7j03r56", "front": "print() vs. return", "back": "print() gibt einen Wert auf der Konsole aus, ohne ihn zurückzugeben. return gibt den Wert an den Aufrufer zurück, ohne ihn anzuzeigen.", "notes": "Eine Funktion mit nur print() liefert None als Rückgabewert.", @@ -103,6 +111,7 @@ }, { "id": "fn-none", + "stable_id": "card-ms942h7j03ssd", "front": "None (Rückgabewert)", "back": "Der Wert, den Python zurückgibt, wenn eine Funktion kein return enthält.", "code_snippet": "def nichts():\n pass\n\nprint(nichts())", @@ -115,6 +124,7 @@ }, { "id": "fn-mehrere-parameter", + "stable_id": "card-ms942h7j03tyn", "front": "Mehrere Parameter", "back": "Eine Funktion kann beliebig viele Parameter durch Kommas getrennt im Funktionskopf haben.", "code_snippet": "def summe(a, b, c):\n return a + b + c\n\nprint(summe(1, 2, 3))", @@ -127,6 +137,7 @@ }, { "id": "fn-scope", + "stable_id": "card-ms942h7j03u41", "front": "Lokaler Geltungsbereich (Scope)", "back": "Variablen, die innerhalb einer Funktion definiert werden, existieren nur dort — außerhalb sind sie nicht sichtbar.", "notes": "Das verhindert, dass Hilfsvariablen in der Funktion globale Namen überschreiben.", @@ -155,6 +166,7 @@ "title": "Funktion definieren", "exercise": { "id": "ex-cloze-def", + "stable_id": "ex-ms942h7j03v1a", "type": "cloze", "prompt": "Ergänze das Schlüsselwort, das eine Funktionsdefinition einleitet.", "card_ids": ["fn-def"], @@ -173,6 +185,7 @@ "title": "Rückgabewert angeben", "exercise": { "id": "ex-cloze-return", + "stable_id": "ex-ms942h7j03wqs", "type": "cloze", "prompt": "Ergänze das Schlüsselwort, das einen Wert aus einer Funktion zurückgibt.", "card_ids": ["fn-return"], @@ -191,6 +204,7 @@ "title": "Parameter vs. Argument", "exercise": { "id": "ex-free-parameter", + "stable_id": "ex-ms942h7j03xxl", "type": "free_text", "prompt": "Wie nennt man die Platzhaltervariable im Funktionskopf (nicht den Wert beim Aufruf)?", "card_ids": ["fn-parameter"], @@ -207,6 +221,7 @@ "title": "print() vs. return", "exercise": { "id": "ex-free-print-vs-return", + "stable_id": "ex-ms942h7j03yoe", "type": "free_text", "prompt": "Was gibt eine Funktion zurück, wenn sie kein return enthält?", "card_ids": ["fn-none", "fn-print-vs-return"], @@ -223,6 +238,7 @@ "title": "Funktionsdefinition zusammensetzen", "exercise": { "id": "ex-tiles-def-funktion", + "stable_id": "ex-ms942h7j03zy1", "type": "word_tiles", "prompt": "Setze eine Funktionsdefinition zusammen, die einen Parameter 'x' hat und x*2 zurückgibt.", "card_ids": ["fn-def", "fn-return"], @@ -238,6 +254,7 @@ "title": "Begriffe rund um Funktionen zuordnen", "exercise": { "id": "ex-match-funktionen", + "stable_id": "ex-ms942h7j040a3", "type": "matching", "prompt": "Ordne jeden Begriff seiner Erklärung zu.", "card_ids": ["fn-def", "fn-parameter", "fn-return", "fn-docstring", "fn-default-arg"], @@ -258,6 +275,7 @@ "title": "Standardwert definieren", "exercise": { "id": "ex-cloze-default", + "stable_id": "ex-ms942h7j041vp", "type": "cloze", "prompt": "Ergänze den Standardwert so, dass die Funktion ohne Argument 'Welt' als Namen verwendet.", "card_ids": ["fn-default-arg"], @@ -276,6 +294,7 @@ "title": "Funktionsaufruf zusammensetzen", "exercise": { "id": "ex-tiles-aufruf", + "stable_id": "ex-ms942h7j04250", "type": "word_tiles", "prompt": "Setze den Funktionsaufruf zusammen, der quadrat mit dem Argument 5 aufruft und das Ergebnis ausgibt.", "card_ids": ["fn-aufruf"], diff --git a/sets/de/python-basics/lessons/09-dictionaries.json b/sets/de/python-basics/lessons/09-dictionaries.json index 4306a64..bb3ae2c 100644 --- a/sets/de/python-basics/lessons/09-dictionaries.json +++ b/sets/de/python-basics/lessons/09-dictionaries.json @@ -9,6 +9,7 @@ "cards": [ { "id": "dict-erstellen", + "stable_id": "card-ms942h7j043f2", "front": "Dictionary erstellen (dict)", "back": "Ein Dictionary speichert Schlüssel-Wert-Paare in geschweiften Klammern.", "code_snippet": "alter = {'Anna': 28, 'Ben': 34}\nprint(alter)", @@ -21,6 +22,7 @@ }, { "id": "dict-zugriff", + "stable_id": "card-ms942h7j044oj", "front": "Wert abrufen — d['schluessel']", "back": "Mit eckigen Klammern und dem Schlüssel greift man auf den zugehörigen Wert zu.", "code_snippet": "alter = {'Anna': 28, 'Ben': 34}\nprint(alter['Anna'])", @@ -33,6 +35,7 @@ }, { "id": "dict-get", + "stable_id": "card-ms942h7j045no", "front": ".get() — sicherer Zugriff", "back": ".get(schluessel) gibt den Wert zurück oder None, wenn der Schlüssel nicht existiert — kein Fehler.", "code_snippet": "alter = {'Anna': 28}\nprint(alter.get('Ben'))", @@ -45,6 +48,7 @@ }, { "id": "dict-hinzufuegen", + "stable_id": "card-ms942h7j046l6", "front": "Neuen Schlüssel hinzufügen", "back": "Mit d['neuer_schluessel'] = wert fügt man ein neues Schlüssel-Wert-Paar hinzu oder überschreibt einen bestehenden Wert.", "code_snippet": "alter = {'Anna': 28}\nalter['Ben'] = 34\nprint(alter['Ben'])", @@ -57,6 +61,7 @@ }, { "id": "dict-keys", + "stable_id": "card-ms942h7j047ui", "front": ".keys() — alle Schlüssel", "back": ".keys() gibt eine Ansicht aller Schlüssel im Dictionary zurück.", "code_snippet": "alter = {'Anna': 28, 'Ben': 34}\nprint(list(alter.keys()))", @@ -69,6 +74,7 @@ }, { "id": "dict-values", + "stable_id": "card-ms942h7j048zg", "front": ".values() — alle Werte", "back": ".values() gibt eine Ansicht aller Werte im Dictionary zurück.", "code_snippet": "alter = {'Anna': 28, 'Ben': 34}\nprint(list(alter.values()))", @@ -81,6 +87,7 @@ }, { "id": "dict-items", + "stable_id": "card-ms942h7j049ab", "front": ".items() — Schlüssel-Wert-Paare", "back": ".items() gibt alle Schlüssel-Wert-Paare als Tupel-Liste zurück — nützlich für Schleifen.", "code_snippet": "alter = {'Anna': 28}\nfor name, a in alter.items():\n print(name, a)", @@ -93,6 +100,7 @@ }, { "id": "dict-in-test", + "stable_id": "card-ms942h7j04aaw", "front": "in-Test auf Schlüssel", "back": "Mit dem in-Operator prüft man, ob ein Schlüssel im Dictionary vorhanden ist.", "code_snippet": "alter = {'Anna': 28, 'Ben': 34}\nprint('Anna' in alter)", @@ -105,6 +113,7 @@ }, { "id": "dict-keyerror", + "stable_id": "card-ms942h7j04by4", "front": "KeyError", "back": "Fehlermeldung, die auftritt, wenn man mit d['schluessel'] auf einen nicht vorhandenen Schlüssel zugreift.", "notes": "Vermeidbar mit .get() oder einem vorherigen in-Test.", @@ -114,6 +123,7 @@ }, { "id": "dict-leer", + "stable_id": "card-ms942h7j04ci6", "front": "Leeres Dictionary", "back": "Ein leeres Dictionary wird mit {} oder dict() erstellt.", "code_snippet": "d = {}\nd['x'] = 10\nprint(d['x'])", @@ -126,6 +136,7 @@ }, { "id": "dict-len", + "stable_id": "card-ms942h7j04da2", "front": "len() bei Dictionaries", "back": "len(d) gibt die Anzahl der Schlüssel-Wert-Paare im Dictionary zurück.", "code_snippet": "alter = {'Anna': 28, 'Ben': 34, 'Clara': 22}\nprint(len(alter))", @@ -157,6 +168,7 @@ "title": "Dictionary-Klammern", "exercise": { "id": "ex-cloze-dict-erstellen", + "stable_id": "ex-ms942h7j04e77", "type": "cloze", "prompt": "Ergänze die Klammern, in denen ein Dictionary steht.", "card_ids": ["dict-erstellen"], @@ -175,6 +187,7 @@ "title": "Sicherer Schlüsselzugriff", "exercise": { "id": "ex-cloze-get", + "stable_id": "ex-ms942h7j04f8v", "type": "cloze", "prompt": "Ergänze die Methode, die einen Wert zurückgibt ohne einen Fehler zu werfen, wenn der Schlüssel fehlt.", "card_ids": ["dict-get"], @@ -193,6 +206,7 @@ "title": "Fehler bei fehlendem Schlüssel", "exercise": { "id": "ex-free-keyerror", + "stable_id": "ex-ms942h7j04g8f", "type": "free_text", "prompt": "Welchen Fehler wirft Python, wenn man mit d['x'] auf einen nicht vorhandenen Schlüssel zugreift?", "card_ids": ["dict-keyerror"], @@ -209,6 +223,7 @@ "title": "Schlüssel prüfen", "exercise": { "id": "ex-free-in-test", + "stable_id": "ex-ms942h7j04hlc", "type": "free_text", "prompt": "Mit welchem Operator prüft man, ob ein Schlüssel in einem Dictionary vorhanden ist?", "card_ids": ["dict-in-test"], @@ -225,6 +240,7 @@ "title": "Wert aus Dictionary abrufen", "exercise": { "id": "ex-tiles-zugriff", + "stable_id": "ex-ms942h7j04i3s", "type": "word_tiles", "prompt": "Setze die Zeile zusammen, die den Wert zum Schlüssel 'Anna' aus dem Dictionary 'alter' ausgibt.", "card_ids": ["dict-zugriff"], @@ -240,6 +256,7 @@ "title": "Dictionary-Methoden zuordnen", "exercise": { "id": "ex-match-methoden", + "stable_id": "ex-ms942h7j04jvf", "type": "matching", "prompt": "Ordne jede Methode ihrer Beschreibung zu.", "card_ids": ["dict-keys", "dict-values", "dict-items", "dict-get"], @@ -259,6 +276,7 @@ "title": "Neuen Eintrag hinzufügen", "exercise": { "id": "ex-tiles-hinzufuegen", + "stable_id": "ex-ms942h7j04kpq", "type": "word_tiles", "prompt": "Setze die Zeile zusammen, die dem Dictionary 'alter' den Schlüssel 'Clara' mit Wert 22 hinzufügt.", "card_ids": ["dict-hinzufuegen"], diff --git a/sets/de/python-basics/lessons/10-tupel-und-mengen.json b/sets/de/python-basics/lessons/10-tupel-und-mengen.json index e40b188..d10355c 100644 --- a/sets/de/python-basics/lessons/10-tupel-und-mengen.json +++ b/sets/de/python-basics/lessons/10-tupel-und-mengen.json @@ -9,6 +9,7 @@ "cards": [ { "id": "tuple-erstellen", + "stable_id": "card-ms942h7k04llb", "front": "Tupel erstellen (tuple)", "back": "Ein Tupel speichert mehrere Werte in runden Klammern. Es ist unveränderlich — nach der Erstellung können keine Elemente hinzugefügt oder entfernt werden.", "code_snippet": "punkt = (3, 7)\nprint(punkt)", @@ -21,6 +22,7 @@ }, { "id": "tuple-index", + "stable_id": "card-ms942h7k04m6z", "front": "Tupel-Element abrufen", "back": "Auf Tupel-Elemente greift man wie bei Listen per Index zu. Der erste Index ist 0.", "code_snippet": "punkt = (3, 7)\nprint(punkt[0])", @@ -33,6 +35,7 @@ }, { "id": "tuple-unveraenderlich", + "stable_id": "card-ms942h7k04n54", "front": "Unveränderlichkeit (immutable)", "back": "Ein Tupel kann nach der Erstellung nicht verändert werden. Der Versuch, ein Element zu ändern, führt zu einem TypeError.", "notes": "Das macht Tupel sicherer als Listen, wenn die Daten konstant bleiben sollen.", @@ -42,6 +45,7 @@ }, { "id": "tuple-vs-liste", + "stable_id": "card-ms942h7k04oxv", "front": "Tupel vs. Liste", "back": "Listen sind veränderlich (mutable) und stehen in [ ]. Tupel sind unveränderlich (immutable) und stehen in ( ). Beide sind geordnet und erlauben doppelte Werte.", "media_type": "text", @@ -50,6 +54,7 @@ }, { "id": "set-erstellen", + "stable_id": "card-ms942h7k04phb", "front": "Menge erstellen (set)", "back": "Eine Menge speichert eindeutige Elemente ohne feste Reihenfolge. Sie wird mit geschweiften Klammern oder set() erstellt.", "code_snippet": "farben = {'rot', 'gruen', 'blau'}\nprint(len(farben))", @@ -62,6 +67,7 @@ }, { "id": "set-duplikate", + "stable_id": "card-ms942h7k04q9t", "front": "Duplikate entfernen mit set()", "back": "set() aus einer Liste erstellt eine Menge und entfernt dabei alle doppelten Werte automatisch.", "code_snippet": "zahlen = [1, 2, 2, 3, 3, 3]\neindeutig = set(zahlen)\nprint(len(eindeutig))", @@ -74,6 +80,7 @@ }, { "id": "set-in-test", + "stable_id": "card-ms942h7k04rrv", "front": "Mengenzugehörigkeit prüfen", "back": "Mit dem in-Operator prüft man sehr effizient, ob ein Element in einer Menge enthalten ist.", "code_snippet": "farben = {'rot', 'gruen', 'blau'}\nprint('rot' in farben)", @@ -86,6 +93,7 @@ }, { "id": "set-vereinigung", + "stable_id": "card-ms942h7k04s5q", "front": "Vereinigung (|) — set union", "back": "Der Operator | gibt eine neue Menge zurück, die alle Elemente beider Mengen enthält.", "code_snippet": "a = {1, 2, 3}\nb = {3, 4, 5}\nprint(len(a | b))", @@ -98,6 +106,7 @@ }, { "id": "set-schnittmenge", + "stable_id": "card-ms942h7k04tph", "front": "Schnittmenge (&) — set intersection", "back": "Der Operator & gibt eine Menge zurück, die nur die Elemente enthält, die in beiden Mengen vorkommen.", "code_snippet": "a = {1, 2, 3}\nb = {2, 3, 4}\nprint(a & b == {2, 3})", @@ -110,6 +119,7 @@ }, { "id": "set-ungeordnet", + "stable_id": "card-ms942h7k04uy7", "front": "Mengen sind ungeordnet", "back": "Elemente in einer Menge haben keine feste Reihenfolge. Man kann nicht mit einem Index auf ein einzelnes Element zugreifen.", "notes": "Deshalb sind Mengenelemente nicht über einen Index abrufbar — dafür gibt es keine Garantie für die Reihenfolge.", @@ -119,6 +129,7 @@ }, { "id": "set-len", + "stable_id": "card-ms942h7k04vpf", "front": "Anzahl der Mengenelemente", "back": "len(menge) gibt die Anzahl der eindeutigen Elemente in der Menge zurück.", "code_snippet": "s = {1, 1, 2, 3}\nprint(len(s))", @@ -156,6 +167,7 @@ "title": "Tupel-Klammern", "exercise": { "id": "ex-cloze-tuple", + "stable_id": "ex-ms942h7k04wyx", "type": "cloze", "prompt": "Ergänze die Klammern, in denen ein Tupel steht.", "card_ids": ["tuple-erstellen"], @@ -174,6 +186,7 @@ "title": "Duplikate entfernen", "exercise": { "id": "ex-cloze-set-duplikate", + "stable_id": "ex-ms942h7k04x7g", "type": "cloze", "prompt": "Ergänze die Funktion, die aus einer Liste eine Menge ohne Duplikate erstellt.", "card_ids": ["set-duplikate"], @@ -192,6 +205,7 @@ "title": "Tupel-Eigenschaft", "exercise": { "id": "ex-free-unveraenderlich", + "stable_id": "ex-ms942h7k04y48", "type": "free_text", "prompt": "Welche Eigenschaft unterscheidet ein Tupel von einer Liste — Tupel können nicht verändert werden?", "card_ids": ["tuple-unveraenderlich", "tuple-vs-liste"], @@ -208,6 +222,7 @@ "title": "Mengeneigenschaft", "exercise": { "id": "ex-free-set-eigenschaft", + "stable_id": "ex-ms942h7k04zix", "type": "free_text", "prompt": "Was ist das besondere Merkmal einer Menge (set) in Bezug auf ihre Elemente?", "card_ids": ["set-erstellen", "set-duplikate"], @@ -224,6 +239,7 @@ "title": "Menge aus Liste erstellen", "exercise": { "id": "ex-tiles-set-erstellen", + "stable_id": "ex-ms942h7k050s8", "type": "word_tiles", "prompt": "Setze die Zeile zusammen, die aus der Liste 'zahlen' eine Menge erstellt und deren Länge ausgibt.", "card_ids": ["set-duplikate"], @@ -239,6 +255,7 @@ "title": "Datenstrukturen zuordnen", "exercise": { "id": "ex-match-datenstrukturen", + "stable_id": "ex-ms942h7k051jz", "type": "matching", "prompt": "Ordne jede Beschreibung der richtigen Datenstruktur zu.", "card_ids": ["tuple-erstellen", "tuple-unveraenderlich", "set-erstellen", "set-ungeordnet"], @@ -258,6 +275,7 @@ "title": "Mengenvereinigung", "exercise": { "id": "ex-tiles-vereinigung", + "stable_id": "ex-ms942h7k052yk", "type": "word_tiles", "prompt": "Setze den Ausdruck zusammen, der die Vereinigung der Mengen a und b bildet und deren Länge ausgibt.", "card_ids": ["set-vereinigung"], diff --git a/sets/de/python-basics/lessons/11-fehler-und-ausnahmen.json b/sets/de/python-basics/lessons/11-fehler-und-ausnahmen.json index 955fe40..95ae7c5 100644 --- a/sets/de/python-basics/lessons/11-fehler-und-ausnahmen.json +++ b/sets/de/python-basics/lessons/11-fehler-und-ausnahmen.json @@ -9,6 +9,7 @@ "cards": [ { "id": "exc-ausnahme", + "stable_id": "card-ms942h7l053fz", "front": "Ausnahme (Exception)", "back": "Ein Fehler, der während der Programmausführung auftritt und den normalen Ablauf unterbricht.", "notes": "Python unterscheidet zwischen Syntaxfehlern (vor der Ausführung) und Ausnahmen (zur Laufzeit).", @@ -18,6 +19,7 @@ }, { "id": "exc-try-except", + "stable_id": "card-ms942h7l0542l", "front": "try / except", "back": "Der try-Block enthält Code, der einen Fehler verursachen könnte. Der except-Block fängt den Fehler ab und reagiert darauf.", "code_snippet": "try:\n x = int('abc')\nexcept ValueError:\n print('Ungültige Zahl')", @@ -30,6 +32,7 @@ }, { "id": "exc-valueerror", + "stable_id": "card-ms942h7l05529", "front": "ValueError", "back": "Wird ausgelöst, wenn ein Wert den richtigen Typ hat, aber einen unzulässigen Inhalt besitzt — z. B. int('abc').", "code_snippet": "try:\n zahl = int('hallo')\nexcept ValueError:\n print('Kein gültiger Zahlenwert')", @@ -42,6 +45,7 @@ }, { "id": "exc-typeerror", + "stable_id": "card-ms942h7l0560w", "front": "TypeError", "back": "Wird ausgelöst, wenn eine Operation auf einem Objekt des falschen Typs angewendet wird — z. B. '2' + 2.", "code_snippet": "try:\n ergebnis = '2' + 2\nexcept TypeError:\n print('Typen passen nicht zusammen')", @@ -54,6 +58,7 @@ }, { "id": "exc-zerodivision", + "stable_id": "card-ms942h7l057x0", "front": "ZeroDivisionError", "back": "Wird ausgelöst, wenn man durch null teilt.", "code_snippet": "try:\n x = 10 / 0\nexcept ZeroDivisionError:\n print('Division durch null!')", @@ -66,6 +71,7 @@ }, { "id": "exc-keyerror", + "stable_id": "card-ms942h7l058p7", "front": "KeyError", "back": "Wird ausgelöst, wenn auf einen Schlüssel (Key) in einem Dictionary zugegriffen wird, der nicht existiert.", "code_snippet": "d = {'a': 1}\ntry:\n print(d['b'])\nexcept KeyError:\n print('Schlüssel nicht gefunden')", @@ -78,6 +84,7 @@ }, { "id": "exc-indexerror", + "stable_id": "card-ms942h7l059b8", "front": "IndexError", "back": "Wird ausgelöst, wenn auf einen Index zugegriffen wird, der außerhalb des gültigen Bereichs einer Liste liegt.", "code_snippet": "liste = [1, 2, 3]\ntry:\n print(liste[10])\nexcept IndexError:\n print('Index außerhalb des Bereichs')", @@ -90,6 +97,7 @@ }, { "id": "exc-else", + "stable_id": "card-ms942h7l05a5m", "front": "else-Zweig im try / except", "back": "Der else-Block wird ausgeführt, wenn im try-Block kein Fehler aufgetreten ist.", "code_snippet": "try:\n x = int('42')\nexcept ValueError:\n print('Fehler')\nelse:\n print('Erfolgreich:', x)", @@ -102,6 +110,7 @@ }, { "id": "exc-finally", + "stable_id": "card-ms942h7l05bx6", "front": "finally", "back": "Der finally-Block wird immer ausgeführt — egal ob ein Fehler aufgetreten ist oder nicht. Er eignet sich zum Aufräumen von Ressourcen.", "code_snippet": "try:\n x = 10 / 2\nexcept ZeroDivisionError:\n print('Fehler')\nfinally:\n print('Fertig')", @@ -114,6 +123,7 @@ }, { "id": "exc-raise", + "stable_id": "card-ms942h7l05c0k", "front": "raise", "back": "Mit raise löst man bewusst eine Ausnahme aus — z. B. um ungültige Eingaben abzulehnen.", "code_snippet": "def nur_positiv(n):\n if n <= 0:\n raise ValueError('Zahl muss positiv sein')\n return n\n\ntry:\n nur_positiv(-3)\nexcept ValueError as e:\n print(e)", @@ -126,6 +136,7 @@ }, { "id": "exc-except-as", + "stable_id": "card-ms942h7l05d2p", "front": "except ExceptionTyp as e", "back": "Mit 'as e' speichert man das Ausnahme-Objekt in einer Variable und kann seine Fehlermeldung ausgeben.", "code_snippet": "try:\n x = int('xyz')\nexcept ValueError as e:\n print('Fehlerdetail:', e)", @@ -138,6 +149,7 @@ }, { "id": "exc-mehrere-except", + "stable_id": "card-ms942h7l05euh", "front": "Mehrere except-Blöcke", "back": "Man kann mehrere except-Blöcke schreiben, um verschiedene Fehlertypen unterschiedlich zu behandeln.", "notes": "Python prüft die except-Blöcke von oben nach unten und führt den ersten passenden aus.", @@ -172,6 +184,7 @@ "title": "try und except ergänzen", "exercise": { "id": "ex-cloze-try", + "stable_id": "ex-ms942h7l05fap", "type": "cloze", "prompt": "Ergänze die beiden Schlüsselwörter, die einen Fehler-Schutzblock einleiten.", "card_ids": ["exc-try-except"], @@ -191,6 +204,7 @@ "title": "Ausnahmetypen zuordnen", "exercise": { "id": "ex-match-ausnahmen", + "stable_id": "ex-ms942h7l05ggm", "type": "matching", "prompt": "Ordne jeden Ausnahmetyp der Situation zu, in der er auftritt.", "card_ids": ["exc-valueerror", "exc-typeerror", "exc-zerodivision", "exc-keyerror", "exc-indexerror"], @@ -211,6 +225,7 @@ "title": "Was macht finally?", "exercise": { "id": "ex-free-finally", + "stable_id": "ex-ms942h7l05huj", "type": "free_text", "prompt": "Wann wird der finally-Block ausgeführt?", "card_ids": ["exc-finally"], @@ -237,6 +252,7 @@ "title": "Eigenen Fehler auslösen", "exercise": { "id": "ex-cloze-raise", + "stable_id": "ex-ms942h7l05irp", "type": "cloze", "prompt": "Ergänze das Schlüsselwort, mit dem man bewusst eine Ausnahme auslöst.", "card_ids": ["exc-raise"], @@ -255,6 +271,7 @@ "title": "try / except-Block zusammensetzen", "exercise": { "id": "ex-tiles-try-except", + "stable_id": "ex-ms942h7l05jtd", "type": "word_tiles", "prompt": "Setze den try / except-Block zusammen, der einen ValueError abfängt.", "card_ids": ["exc-try-except", "exc-valueerror"], @@ -270,6 +287,7 @@ "title": "else im try-Block", "exercise": { "id": "ex-free-else-except", + "stable_id": "ex-ms942h7l05kfl", "type": "free_text", "prompt": "Wann wird der else-Zweig eines try / except-Blocks ausgeführt?", "card_ids": ["exc-else"], @@ -296,6 +314,7 @@ "title": "try-Blöcke und ihre Bedeutung", "exercise": { "id": "ex-match-blöcke", + "stable_id": "ex-ms942h7l05lco", "type": "matching", "prompt": "Ordne jeden Block seiner Funktion zu.", "card_ids": ["exc-try-except", "exc-else", "exc-finally", "exc-raise"], @@ -316,6 +335,7 @@ "title": "ZeroDivisionError erkennen", "exercise": { "id": "ex-cloze-zerodiv", + "stable_id": "ex-ms942h7l05m41", "type": "cloze", "prompt": "Ergänze den richtigen Ausnahmetyp für eine Division durch null.", "card_ids": ["exc-zerodivision"], diff --git a/sets/de/python-basics/lessons/12-module-und-imports.json b/sets/de/python-basics/lessons/12-module-und-imports.json index 1faab59..33829c2 100644 --- a/sets/de/python-basics/lessons/12-module-und-imports.json +++ b/sets/de/python-basics/lessons/12-module-und-imports.json @@ -9,6 +9,7 @@ "cards": [ { "id": "mod-modul", + "stable_id": "card-ms942h7l05n0k", "front": "Modul (Module)", "back": "Eine Python-Datei, die Funktionen, Klassen und Variablen enthält und in andere Programme eingebunden werden kann.", "notes": "Jede .py-Datei ist ein Modul. Die Standardbibliothek liefert viele fertige Module.", @@ -18,6 +19,7 @@ }, { "id": "mod-import", + "stable_id": "card-ms942h7l05ong", "front": "import modul", "back": "Bindet ein ganzes Modul ein. Auf Inhalte greift man mit modul.name zu.", "code_snippet": "import math\nprint(math.pi)", @@ -30,6 +32,7 @@ }, { "id": "mod-from-import", + "stable_id": "card-ms942h7l05p09", "front": "from modul import name", "back": "Importiert einen einzelnen Namen direkt in den aktuellen Namensraum — kein Modulpräfix nötig.", "code_snippet": "from math import sqrt\nprint(sqrt(25))", @@ -42,6 +45,7 @@ }, { "id": "mod-alias", + "stable_id": "card-ms942h7l05qli", "front": "import ... as (Alias)", "back": "Gibt einem Modul oder Namen beim Import einen kürzeren Alternativnamen (Alias).", "code_snippet": "import math as m\nprint(m.sqrt(9))", @@ -54,6 +58,7 @@ }, { "id": "mod-math-sqrt", + "stable_id": "card-ms942h7l05r86", "front": "math.sqrt()", "back": "Berechnet die Quadratwurzel einer Zahl. Gibt immer einen float zurück.", "code_snippet": "import math\nprint(math.sqrt(16))", @@ -66,6 +71,7 @@ }, { "id": "mod-math-pi", + "stable_id": "card-ms942h7l05sql", "front": "math.pi", "back": "Die mathematische Konstante π (Pi) als float — ungefähr 3,14159.", "code_snippet": "import math\nprint(math.pi)", @@ -78,6 +84,7 @@ }, { "id": "mod-random-modul", + "stable_id": "card-ms942h7l05t2i", "front": "random-Modul", "back": "Standardmodul für Zufallszahlen und zufällige Auswahl. Wichtige Funktion: random.randint(a, b) liefert eine zufällige ganze Zahl von a bis b (einschließlich).", "notes": "Da die Ergebnisse nicht deterministisch sind, prüft man random-Ausgaben nur manuell oder mit Seed.", @@ -87,6 +94,7 @@ }, { "id": "mod-standardbibliothek", + "stable_id": "card-ms942h7l05ufh", "front": "Standardbibliothek (Standard Library)", "back": "Eine umfangreiche Sammlung von Modulen, die mit Python ausgeliefert wird — kein separater Install nötig.", "notes": "Beispiele: math, random, os, datetime, json, csv.", @@ -96,6 +104,7 @@ }, { "id": "mod-paket", + "stable_id": "card-ms942h7l05v74", "front": "Paket (Package)", "back": "Eine Sammlung von Modulen in einem Verzeichnis. Wird genauso importiert wie ein Modul.", "notes": "Ein Verzeichnis wird zum Paket, wenn es eine __init__.py-Datei enthält (in modernem Python oft nicht mehr nötig).", @@ -105,6 +114,7 @@ }, { "id": "mod-from-import-mehrere", + "stable_id": "card-ms942h7l05wqo", "front": "from modul import name1, name2", "back": "Mehrere Namen aus einem Modul auf einmal importieren — durch Kommas getrennt.", "code_snippet": "from math import sqrt, pi\nprint(sqrt(4))\nprint(pi)", @@ -117,6 +127,7 @@ }, { "id": "mod-namensraum", + "stable_id": "card-ms942h7l05xno", "front": "Namensraum (Namespace)", "back": "Der Bereich, in dem ein Name (Variable, Funktion) gültig ist. Module haben einen eigenen Namensraum, der Namenskonflikte vermeidet.", "notes": "Deshalb ist math.sqrt sicher, auch wenn man selbst eine Funktion sqrt definiert.", @@ -145,6 +156,7 @@ "title": "Das import-Schlüsselwort", "exercise": { "id": "ex-cloze-import", + "stable_id": "ex-ms942h7l05yzv", "type": "cloze", "prompt": "Ergänze das Schlüsselwort, das ein Modul einbindet.", "card_ids": ["mod-import"], @@ -163,6 +175,7 @@ "title": "from ... import ergänzen", "exercise": { "id": "ex-cloze-from-import", + "stable_id": "ex-ms942h7l05zrb", "type": "cloze", "prompt": "Ergänze die beiden Schlüsselwörter, um nur sqrt aus math zu laden.", "card_ids": ["mod-from-import"], @@ -182,6 +195,7 @@ "title": "Import-Formen zuordnen", "exercise": { "id": "ex-match-importsyntax", + "stable_id": "ex-ms942h7l060dg", "type": "matching", "prompt": "Ordne jede Import-Form ihrer Beschreibung zu.", "card_ids": ["mod-import", "mod-from-import", "mod-alias"], @@ -200,6 +214,7 @@ "title": "Quadratwurzel mit math", "exercise": { "id": "ex-free-sqrt", + "stable_id": "ex-ms942h7l061ay", "type": "free_text", "prompt": "Welchen Wert gibt math.sqrt(16) zurück?", "card_ids": ["mod-math-sqrt"], @@ -216,6 +231,7 @@ "title": "Import-Zeile zusammensetzen", "exercise": { "id": "ex-tiles-import-math", + "stable_id": "ex-ms942h7l062r6", "type": "word_tiles", "prompt": "Setze die Zeile zusammen, die sqrt direkt aus math importiert.", "card_ids": ["mod-from-import"], @@ -231,6 +247,7 @@ "title": "Was ist ein Alias?", "exercise": { "id": "ex-free-alias", + "stable_id": "ex-ms942h7l063a0", "type": "free_text", "prompt": "Welches Schlüsselwort gibt einem importierten Modul einen anderen Namen?", "card_ids": ["mod-alias"], @@ -247,6 +264,7 @@ "title": "Modulbegriffe zuordnen", "exercise": { "id": "ex-match-module-begriffe", + "stable_id": "ex-ms942h7l064ti", "type": "matching", "prompt": "Ordne jeden Begriff seiner Erklärung zu.", "card_ids": ["mod-modul", "mod-standardbibliothek", "mod-paket", "mod-namensraum"], @@ -266,6 +284,7 @@ "title": "Auf math.pi zugreifen", "exercise": { "id": "ex-cloze-mathpi", + "stable_id": "ex-ms942h7l065bp", "type": "cloze", "prompt": "Ergänze den Ausdruck, der die Kreiszahl Pi aus dem math-Modul liefert.", "card_ids": ["mod-math-pi"], diff --git a/sets/de/python-basics/lessons/13-dateien.json b/sets/de/python-basics/lessons/13-dateien.json index 8154424..d37f299 100644 --- a/sets/de/python-basics/lessons/13-dateien.json +++ b/sets/de/python-basics/lessons/13-dateien.json @@ -9,6 +9,7 @@ "cards": [ { "id": "dat-open", + "stable_id": "card-ms942h7m0663t", "front": "open(dateiname, modus)", "back": "Öffnet eine Datei und gibt ein Datei-Objekt zurück. Der Modus bestimmt, ob man liest ('r'), schreibt ('w') oder anhängt ('a').", "notes": "Standardmodus ist 'r' (lesen). Wichtig: Die Datei sollte immer geschlossen werden.", @@ -18,6 +19,7 @@ }, { "id": "dat-modus-r", + "stable_id": "card-ms942h7m067df", "front": "Modus 'r' (read)", "back": "Öffnet eine Datei zum Lesen. Die Datei muss bereits existieren, sonst gibt es einen FileNotFoundError.", "media_type": "text", @@ -26,6 +28,7 @@ }, { "id": "dat-modus-w", + "stable_id": "card-ms942h7m068h9", "front": "Modus 'w' (write)", "back": "Öffnet eine Datei zum Schreiben. Existiert die Datei bereits, wird ihr Inhalt überschrieben. Existiert sie nicht, wird sie neu erstellt.", "media_type": "text", @@ -34,6 +37,7 @@ }, { "id": "dat-modus-a", + "stable_id": "card-ms942h7m069zj", "front": "Modus 'a' (append)", "back": "Öffnet eine Datei zum Anhängen. Neuer Text wird am Ende eingefügt; vorhandener Inhalt bleibt erhalten.", "media_type": "text", @@ -42,6 +46,7 @@ }, { "id": "dat-read", + "stable_id": "card-ms942h7m06a0t", "front": ".read()", "back": "Liest den gesamten Inhalt einer Datei als einen einzigen String.", "notes": "Bei großen Dateien kann .read() viel Arbeitsspeicher belegen.", @@ -51,6 +56,7 @@ }, { "id": "dat-readline", + "stable_id": "card-ms942h7m06b9w", "front": ".readline()", "back": "Liest eine einzelne Zeile aus der Datei (inklusive dem Zeilenumbruch am Ende).", "notes": "Jeder Aufruf von readline() liest die nächste Zeile.", @@ -60,6 +66,7 @@ }, { "id": "dat-readlines", + "stable_id": "card-ms942h7m06cmc", "front": ".readlines()", "back": "Liest alle Zeilen und gibt sie als Liste von Strings zurück. Jede Zeile enthält am Ende einen Zeilenumbruch (\\n).", "notes": "Praktisch, wenn man alle Zeilen auf einmal in einer Liste haben möchte.", @@ -69,6 +76,7 @@ }, { "id": "dat-write", + "stable_id": "card-ms942h7m06dkj", "front": ".write(text)", "back": "Schreibt einen String in die Datei. Zeilenumbrüche müssen manuell mit \\n eingefügt werden.", "notes": ".write() gibt die Anzahl der geschriebenen Zeichen zurück.", @@ -78,6 +86,7 @@ }, { "id": "dat-with", + "stable_id": "card-ms942h7m06eml", "front": "with open(...) as f:", "back": "Das with-Statement öffnet eine Datei als Context Manager. Die Datei wird automatisch geschlossen, sobald der with-Block endet — auch bei einem Fehler.", "code_snippet": "with open('notiz.txt', 'w') as f:\n f.write('Hallo Datei')\nprint('Datei geschlossen')", @@ -90,6 +99,7 @@ }, { "id": "dat-context-manager", + "stable_id": "card-ms942h7m06fhi", "front": "Context Manager", "back": "Ein Objekt, das mit dem with-Statement zusammenarbeitet und automatisch Auf- und Abbau-Aktionen ausführt — z. B. eine Datei sicher schließen.", "notes": "open() gibt einen Context Manager zurück. Eigene Context Manager kann man mit __enter__ und __exit__ erstellen.", @@ -99,6 +109,7 @@ }, { "id": "dat-iteration", + "stable_id": "card-ms942h7m06glh", "front": "Über Zeilen einer Datei iterieren", "back": "Mit einer for-Schleife kann man direkt über ein geöffnetes Datei-Objekt iterieren — Python liefert dabei Zeile für Zeile.", "code_snippet": "with open('liste.txt', 'w') as f:\n f.write('Apfel\\nBanane\\nKirsche')\n\nwith open('liste.txt', 'r') as f:\n for zeile in f:\n print(zeile.strip())", @@ -111,6 +122,7 @@ }, { "id": "dat-close", + "stable_id": "card-ms942h7m06hxb", "front": ".close()", "back": "Schließt eine geöffnete Datei manuell. Mit dem with-Statement ist .close() nicht mehr nötig, da es automatisch aufgerufen wird.", "notes": "Nicht geschlossene Dateien können zu Datenverlust oder Speicherfehlern führen.", @@ -145,6 +157,7 @@ "title": "Dateimodi zuordnen", "exercise": { "id": "ex-match-modi", + "stable_id": "ex-ms942h7m06ias", "type": "matching", "prompt": "Ordne jeden Modus seiner Bedeutung zu.", "card_ids": ["dat-modus-r", "dat-modus-w", "dat-modus-a"], @@ -163,6 +176,7 @@ "title": "with-Statement ergänzen", "exercise": { "id": "ex-cloze-with", + "stable_id": "ex-ms942h7m06jc5", "type": "cloze", "prompt": "Ergänze das Schlüsselwort für das sichere Öffnen von Dateien.", "card_ids": ["dat-with"], @@ -181,6 +195,7 @@ "title": "Vorteil des with-Statements", "exercise": { "id": "ex-free-with-vorteil", + "stable_id": "ex-ms942h7m06k2f", "type": "free_text", "prompt": "Was ist der wichtigste Vorteil des with-Statements beim Öffnen von Dateien?", "card_ids": ["dat-with", "dat-context-manager"], @@ -207,6 +222,7 @@ "title": "Datei schreiben zusammensetzen", "exercise": { "id": "ex-tiles-write", + "stable_id": "ex-ms942h7m06lkj", "type": "word_tiles", "prompt": "Setze die Zeile zusammen, die eine Datei zum Schreiben öffnet.", "card_ids": ["dat-with", "dat-modus-w"], @@ -222,6 +238,7 @@ "title": "Lese-Methoden zuordnen", "exercise": { "id": "ex-match-lese-methoden", + "stable_id": "ex-ms942h7m06mrw", "type": "matching", "prompt": "Ordne jede Methode ihrem Rückgabewert zu.", "card_ids": ["dat-read", "dat-readline", "dat-readlines"], @@ -240,6 +257,7 @@ "title": "Den Schreibmodus kennen", "exercise": { "id": "ex-cloze-modus-w", + "stable_id": "ex-ms942h7m06no2", "type": "cloze", "prompt": "Ergänze den Modus, der eine Datei zum Schreiben öffnet (und vorhandenen Inhalt überschreibt).", "card_ids": ["dat-modus-w"], @@ -258,6 +276,7 @@ "title": "Was gibt readlines() zurück?", "exercise": { "id": "ex-free-readlines", + "stable_id": "ex-ms942h7m06owc", "type": "free_text", "prompt": "Was gibt die Methode .readlines() zurück?", "card_ids": ["dat-readlines"], diff --git a/sets/de/python-basics/lessons/14-comprehensions.json b/sets/de/python-basics/lessons/14-comprehensions.json index e8ffa21..4be9faf 100644 --- a/sets/de/python-basics/lessons/14-comprehensions.json +++ b/sets/de/python-basics/lessons/14-comprehensions.json @@ -9,6 +9,7 @@ "cards": [ { "id": "py-list-comp-basis", + "stable_id": "card-ms942h7m06p03", "front": "List Comprehension (Grundform)", "back": "Eine kompakte Schreibweise, um aus einer bestehenden Liste eine neue zu erzeugen: [Ausdruck for Element in Iterable].", "code_snippet": "zahlen = [1, 2, 3, 4]\ndoppelt = [x * 2 for x in zahlen]\nprint(doppelt)", @@ -21,6 +22,7 @@ }, { "id": "py-list-comp-bedingung", + "stable_id": "card-ms942h7m06qy1", "front": "List Comprehension mit Bedingung (if)", "back": "Mit einem optionalen if-Teil filtert man Elemente: [Ausdruck for Element in Iterable if Bedingung].", "code_snippet": "zahlen = [1, 2, 3, 4, 5, 6]\ngerade = [x for x in zahlen if x % 2 == 0]\nprint(gerade)", @@ -33,6 +35,7 @@ }, { "id": "py-list-comp-vs-for", + "stable_id": "card-ms942h7m06rk2", "front": "List Comprehension vs. for-Schleife", "back": "Beide erzeugen eine neue Liste. Die Comprehension ist kürzer und oft lesbarer; die klassische for-Schleife ist flexibler bei komplexer Logik.", "code_snippet": "zahlen = [1, 2, 3]\n# klassisch:\nquadrate = []\nfor x in zahlen:\n quadrate.append(x ** 2)\n# Comprehension:\nquadrate2 = [x ** 2 for x in zahlen]\nprint(quadrate)\nprint(quadrate2)", @@ -45,6 +48,7 @@ }, { "id": "py-list-comp-string", + "stable_id": "card-ms942h7m06sql", "front": "List Comprehension mit Strings", "back": "Man kann auch über eine Liste von Strings iterieren und jeden Eintrag umwandeln, z. B. in Großbuchstaben.", "code_snippet": "woerter = ['hallo', 'welt', 'python']\ngross = [w.upper() for w in woerter]\nprint(gross)", @@ -57,6 +61,7 @@ }, { "id": "py-dict-comp", + "stable_id": "card-ms942h7m06tmv", "front": "Dict Comprehension", "back": "Wie eine List Comprehension, aber für Dictionaries: {Schlüssel: Wert for Element in Iterable}.", "code_snippet": "zahlen = [1, 2, 3]\nquadrate = {x: x ** 2 for x in zahlen}\nprint(quadrate)", @@ -69,6 +74,7 @@ }, { "id": "py-comp-iterable", + "stable_id": "card-ms942h7m06u8t", "front": "Iterable (in Comprehensions)", "back": "Jedes Objekt, über das man mit for iterieren kann: Listen, Strings, range() usw.", "notes": "In [x*2 for x in zahlen] ist zahlen das Iterable.", @@ -78,6 +84,7 @@ }, { "id": "py-comp-range", + "stable_id": "card-ms942h7m06vx9", "front": "List Comprehension mit range()", "back": "range() lässt sich direkt als Iterable in einer Comprehension nutzen.", "code_snippet": "quadrate = [x ** 2 for x in range(1, 6)]\nprint(quadrate)", @@ -90,6 +97,7 @@ }, { "id": "py-comp-filter-string", + "stable_id": "card-ms942h7m06wsv", "front": "List Comprehension: Strings filtern", "back": "Mit einer if-Bedingung filtert man Elemente nach einer Eigenschaft, z. B. nur lange Wörter behalten.", "code_snippet": "woerter = ['Hi', 'Python', 'ist', 'toll']\nlang = [w for w in woerter if len(w) > 3]\nprint(lang)", @@ -102,6 +110,7 @@ }, { "id": "py-comp-vorteil", + "stable_id": "card-ms942h7m06xwb", "front": "Vorteile von Comprehensions", "back": "Kürzerer Code, oft bessere Lesbarkeit und in der Regel etwas schneller als eine gleichwertige for-Schleife.", "notes": "Bei sehr komplexer Logik ist die klassische Schleife verständlicher.", @@ -111,6 +120,7 @@ }, { "id": "py-comp-ergebnis-typ", + "stable_id": "card-ms942h7m06yk1", "front": "Ergebnistyp einer List Comprehension", "back": "Eine List Comprehension gibt immer eine neue Liste (list) zurück — unabhängig vom Typ des Iterables.", "code_snippet": "ergebnis = [c for c in 'abc']\nprint(ergebnis)\nprint(type(ergebnis))", @@ -142,6 +152,7 @@ "title": "Grundform der Comprehension", "exercise": { "id": "ex-cloze-comp-basis", + "stable_id": "ex-ms942h7m06znf", "type": "cloze", "prompt": "Ergänze das Schlüsselwort, das in jeder List Comprehension steht.", "card_ids": ["py-list-comp-basis"], @@ -160,6 +171,7 @@ "title": "Filtern mit Bedingung", "exercise": { "id": "ex-cloze-comp-bedingung", + "stable_id": "ex-ms942h7m070lh", "type": "cloze", "prompt": "Ergänze das Schlüsselwort, mit dem man in einer Comprehension Elemente filtert.", "card_ids": ["py-list-comp-bedingung"], @@ -178,6 +190,7 @@ "title": "Welchen Typ gibt eine List Comprehension zurück?", "exercise": { "id": "ex-free-comp-ergebnis", + "stable_id": "ex-ms942h7m071po", "type": "free_text", "prompt": "Welchen Datentyp gibt eine List Comprehension immer zurück?", "card_ids": ["py-comp-ergebnis-typ"], @@ -194,6 +207,7 @@ "title": "Dict Comprehension erkennen", "exercise": { "id": "ex-free-dict-comp", + "stable_id": "ex-ms942h7m0725e", "type": "free_text", "prompt": "Welche Art von Klammern verwendet man bei einer Dict Comprehension?", "card_ids": ["py-dict-comp"], @@ -210,6 +224,7 @@ "title": "Comprehension mit Filter zusammensetzen", "exercise": { "id": "ex-tiles-comp-gerade", + "stable_id": "ex-ms942h7m073te", "type": "word_tiles", "prompt": "Setze die List Comprehension zusammen, die nur gerade Zahlen aus 'zahlen' auswählt.", "card_ids": ["py-list-comp-bedingung"], @@ -225,6 +240,7 @@ "title": "Comprehension-Arten zuordnen", "exercise": { "id": "ex-match-comp-typen", + "stable_id": "ex-ms942h7m074w8", "type": "matching", "prompt": "Ordne jede Comprehension-Schreibweise dem richtigen Ergebnistyp zu.", "card_ids": ["py-list-comp-basis", "py-dict-comp", "py-comp-ergebnis-typ"], @@ -243,6 +259,7 @@ "title": "Begriffe der Comprehension zuordnen", "exercise": { "id": "ex-match-comp-begriffe", + "stable_id": "ex-ms942h7m075ww", "type": "matching", "prompt": "Ordne jeden Begriff seiner Rolle in einer List Comprehension zu.", "card_ids": ["py-list-comp-basis", "py-list-comp-bedingung", "py-comp-iterable"], diff --git a/sets/de/python-basics/lessons/15-wiederholung.json b/sets/de/python-basics/lessons/15-wiederholung.json index 3e34f18..4486c86 100644 --- a/sets/de/python-basics/lessons/15-wiederholung.json +++ b/sets/de/python-basics/lessons/15-wiederholung.json @@ -9,6 +9,7 @@ "cards": [ { "id": "rep-print-variable", + "stable_id": "card-ms942h7n076te", "front": "print() und Variablen", "back": "print() gibt Werte auf der Konsole aus. Variablen speichern Werte mit = und können jederzeit überschrieben werden.", "code_snippet": "name = 'Anna'\nalter = 25\nprint(name, alter)", @@ -21,6 +22,7 @@ }, { "id": "rep-datentypen", + "stable_id": "card-ms942h7n0779s", "front": "Grundlegende Datentypen", "back": "int (ganze Zahl), float (Dezimalzahl), str (Text), bool (True/False), list (Liste), dict (Wörterbuch).", "notes": "type() zeigt den Datentyp einer Variablen an.", @@ -30,6 +32,7 @@ }, { "id": "rep-operatoren", + "stable_id": "card-ms942h7n078e3", "front": "Arithmetische und Vergleichsoperatoren", "back": "Arithmetik: + - * / // % **. Vergleiche: == != < > <= >= liefern True oder False.", "code_snippet": "x = 10\nprint(x % 3)\nprint(x > 5)", @@ -42,6 +45,7 @@ }, { "id": "rep-strings", + "stable_id": "card-ms942h7n0799q", "front": "Strings: Methoden und f-Strings", "back": "Strings haben Methoden wie .upper(), .lower(), .strip(), .replace(). f-Strings erlauben das Einbetten von Variablen: f'Hallo {name}'.", "code_snippet": "name = 'welt'\nprint(f'Hallo {name.upper()}')", @@ -54,6 +58,7 @@ }, { "id": "rep-listen", + "stable_id": "card-ms942h7n07av4", "front": "Listen: erstellen, zugreifen, verändern", "back": "Listen speichern mehrere Werte in eckigen Klammern. Zugriff per Index (ab 0). Methoden: append(), remove(), len().", "code_snippet": "zahlen = [3, 1, 4]\nzahlen.append(2)\nprint(zahlen)\nprint(len(zahlen))", @@ -66,6 +71,7 @@ }, { "id": "rep-bedingungen", + "stable_id": "card-ms942h7n07b52", "front": "Bedingte Anweisungen (if / elif / else)", "back": "if prüft eine Bedingung, elif prüft weitere Fälle, else greift, wenn keine Bedingung zutrifft. Einrückung ist Pflicht.", "code_snippet": "x = 7\nif x > 10:\n print('gross')\nelif x > 5:\n print('mittel')\nelse:\n print('klein')", @@ -78,6 +84,7 @@ }, { "id": "rep-schleifen", + "stable_id": "card-ms942h7n07cri", "front": "for- und while-Schleife", "back": "for iteriert über ein Iterable (Liste, range usw.). while wiederholt solange eine Bedingung True ist.", "code_snippet": "for i in range(3):\n print(i)", @@ -90,6 +97,7 @@ }, { "id": "rep-funktionen", + "stable_id": "card-ms942h7n07dgm", "front": "Funktionen (def / return)", "back": "def definiert eine Funktion mit einem Namen und optionalen Parametern. return gibt einen Wert zurück. Ohne return liefert die Funktion None.", "code_snippet": "def quadrat(x):\n return x ** 2\n\nprint(quadrat(5))", @@ -102,6 +110,7 @@ }, { "id": "rep-dictionaries", + "stable_id": "card-ms942h7n07e26", "front": "Dictionaries (dict)", "back": "Ein Dictionary speichert Schlüssel-Wert-Paare in geschweiften Klammern. Zugriff per Schlüssel: d['key']. Methoden: .keys(), .values(), .items().", "code_snippet": "person = {'name': 'Lisa', 'alter': 30}\nprint(person['name'])\nprint(person['alter'])", @@ -114,6 +123,7 @@ }, { "id": "rep-fehlerbehandlung", + "stable_id": "card-ms942h7n07fe7", "front": "Fehlerbehandlung (try / except)", "back": "try enthält risikoreichen Code; except fängt Ausnahmen ab und verhindert einen Programmabsturz.", "code_snippet": "try:\n ergebnis = 10 / 0\nexcept ZeroDivisionError:\n print('Division durch null!')", @@ -126,6 +136,7 @@ }, { "id": "rep-comprehension", + "stable_id": "card-ms942h7n07ggu", "front": "List Comprehension (Kurzform)", "back": "Erzeugt eine neue Liste kompakt: [Ausdruck for Element in Iterable]. Optionales if filtert Elemente.", "code_snippet": "gerade = [x for x in range(1, 8) if x % 2 == 0]\nprint(gerade)", @@ -138,6 +149,7 @@ }, { "id": "rep-typ-konvertierung", + "stable_id": "card-ms942h7n07hsa", "front": "Typkonvertierung (type casting)", "back": "int(), float(), str() und bool() wandeln Werte in einen anderen Typ um.", "code_snippet": "zahl = int('42')\nprint(zahl + 1)", @@ -169,6 +181,7 @@ "title": "Datentypen zuordnen", "exercise": { "id": "ex-match-datentypen", + "stable_id": "ex-ms942h7n07if8", "type": "matching", "prompt": "Ordne jeden Wert seinem Datentyp zu.", "card_ids": ["rep-datentypen", "rep-print-variable"], @@ -188,6 +201,7 @@ "title": "Konzepte zuordnen", "exercise": { "id": "ex-match-konzepte", + "stable_id": "ex-ms942h7n07jd4", "type": "matching", "prompt": "Ordne jedes Python-Konzept seiner Beschreibung zu.", "card_ids": ["rep-funktionen", "rep-dictionaries", "rep-fehlerbehandlung", "rep-comprehension"], @@ -207,6 +221,7 @@ "title": "for-Schleife vervollständigen", "exercise": { "id": "ex-cloze-for-schleife", + "stable_id": "ex-ms942h7n07k7b", "type": "cloze", "prompt": "Ergänze das Schlüsselwort, das diese Schleife einleitet.", "card_ids": ["rep-schleifen"], @@ -225,6 +240,7 @@ "title": "Funktion definieren", "exercise": { "id": "ex-cloze-def", + "stable_id": "ex-ms942h7n07l7u", "type": "cloze", "prompt": "Ergänze das Schlüsselwort, mit dem man in Python eine Funktion definiert.", "card_ids": ["rep-funktionen"], @@ -243,6 +259,7 @@ "title": "Fehlerbehandlung", "exercise": { "id": "ex-free-try-except", + "stable_id": "ex-ms942h7n07mog", "type": "free_text", "prompt": "Mit welchem Schlüsselwort beginnt ein Block, der einen Fehler abfangen soll?", "card_ids": ["rep-fehlerbehandlung"], @@ -259,6 +276,7 @@ "title": "Dictionary-Zugriff", "exercise": { "id": "ex-free-dict-zugriff", + "stable_id": "ex-ms942h7n07n70", "type": "free_text", "prompt": "Wie greift man auf den Wert eines bestimmten Schlüssels in einem Dictionary zu?", "card_ids": ["rep-dictionaries"], @@ -275,6 +293,7 @@ "title": "Bedingungsstruktur zusammensetzen", "exercise": { "id": "ex-tiles-if-elif-else", + "stable_id": "ex-ms942h7n07o9l", "type": "word_tiles", "prompt": "Setze die drei Schlüsselwörter in der richtigen Reihenfolge zusammen, die eine vollständige Bedingungsstruktur bilden.", "card_ids": ["rep-bedingungen"], @@ -290,6 +309,7 @@ "title": "List Comprehension zusammensetzen", "exercise": { "id": "ex-tiles-list-comp", + "stable_id": "ex-ms942h7n07pss", "type": "word_tiles", "prompt": "Setze die List Comprehension zusammen, die jede Zahl aus 'zahlen' verdoppelt.", "card_ids": ["rep-comprehension"], diff --git a/sets/de/react-fortgeschritten/lessons/01-useeffect-verstehen.json b/sets/de/react-fortgeschritten/lessons/01-useeffect-verstehen.json index b78bed8..4561233 100644 --- a/sets/de/react-fortgeschritten/lessons/01-useeffect-verstehen.json +++ b/sets/de/react-fortgeschritten/lessons/01-useeffect-verstehen.json @@ -9,6 +9,7 @@ "cards": [ { "id": "useeffect", + "stable_id": "card-ms942h7o07qzv", "front": "useEffect", "back": "Der Hook für Seiteneffekte in Funktionskomponenten (z. B. Daten laden, Timer, Abos).", "notes": "Englisch: side effect. Läuft nach dem Render, nicht währenddessen.", @@ -21,6 +22,7 @@ }, { "id": "zeitpunkt", + "stable_id": "card-ms942h7o07rup", "front": "Ausführungszeitpunkt", "back": "useEffect läuft NACH dem Render (nachdem der Bildschirm aktualisiert wurde) — nicht davor.", "notes": "Ein häufiger Irrtum ist, useEffect laufe vor dem Render.", @@ -33,6 +35,7 @@ }, { "id": "dep-leer", + "stable_id": "card-ms942h7o07sqp", "front": "Leeres Dependency-Array []", "back": "Der Effekt läuft nur einmal nach dem ersten Render (Mount).", "notes": "Ideal für einmalige Initialisierung.", @@ -45,6 +48,7 @@ }, { "id": "dep-mit", + "stable_id": "card-ms942h7o07tuo", "front": "Dependency-Array mit Werten", "back": "Der Effekt läuft erneut, wenn sich einer der aufgeführten Werte ändert.", "notes": "z. B. [count] — läuft, wenn sich count ändert.", @@ -57,6 +61,7 @@ }, { "id": "dep-ohne", + "stable_id": "card-ms942h7o07uyb", "front": "Kein Dependency-Array", "back": "Lässt man das zweite Argument weg, läuft der Effekt nach JEDEM Render.", "notes": "Selten gewollt; führt leicht zu Endlosschleifen.", @@ -69,6 +74,7 @@ }, { "id": "cleanup", + "stable_id": "card-ms942h7o07vvv", "front": "Cleanup-Funktion", "back": "Die vom Effekt zurückgegebene Funktion räumt auf: sie läuft vor dem nächsten Effekt und beim Unmount.", "notes": "z. B. Timer stoppen, Abo abmelden.", @@ -126,7 +132,7 @@ "Nur beim Klick" ], "hint": "Der Effekt sieht das fertige Render.", - "id": "ex-mc-zeitpunkt" + "id": "ex-mc-zeitpunkt", "stable_id": "ex-ms942h7o07whq" } }, { @@ -155,7 +161,7 @@ "Der Effekt läuft vor dem Render" ], "hint": "„Einmalig nach dem Mount.\"", - "id": "ex-mc-leer" + "id": "ex-mc-leer", "stable_id": "ex-ms942h7o07xwh" } }, { @@ -184,7 +190,7 @@ "Nur beim Unmount" ], "hint": "Fehlt das Array, gibt es keine Begrenzung.", - "id": "ex-mc-ohne" + "id": "ex-mc-ohne", "stable_id": "ex-ms942h7o07yf7" } }, { @@ -213,7 +219,7 @@ "Um einen Re-Render zu erzwingen" ], "hint": "Timer stoppen, Abo abmelden.", - "id": "ex-mc-cleanup" + "id": "ex-mc-cleanup", "stable_id": "ex-ms942h7o07z32" } }, { @@ -241,7 +247,7 @@ "Er blockiert das Rendern der Oberfläche" ], "hint": "Denke an Zeitpunkt, Cleanup und Deps.", - "id": "ex-ms-useeffect" + "id": "ex-ms-useeffect", "stable_id": "ex-ms942h7o080tw" } }, { @@ -266,7 +272,7 @@ "Nie" ], "hint": "Nur der Wert count steuert die Wiederholung.", - "id": "ex-ms-deps" + "id": "ex-ms-deps", "stable_id": "ex-ms942h7o081ij" } }, { @@ -297,7 +303,7 @@ } ], "direction": "target_to_source", - "id": "ex-match-deps" + "id": "ex-match-deps", "stable_id": "ex-ms942h7o082gb" } }, { @@ -321,7 +327,7 @@ ], "cloze_mode": "type", "hint": "Beginnt mit „use\".", - "id": "ex-tip-name" + "id": "ex-tip-name", "stable_id": "ex-ms942h7o08356" } } ] diff --git a/sets/de/react-fortgeschritten/lessons/02-datenfetching.json b/sets/de/react-fortgeschritten/lessons/02-datenfetching.json index e51439b..92a67e9 100644 --- a/sets/de/react-fortgeschritten/lessons/02-datenfetching.json +++ b/sets/de/react-fortgeschritten/lessons/02-datenfetching.json @@ -9,6 +9,7 @@ "cards": [ { "id": "fetch-effect", + "stable_id": "card-ms942h7o084bg", "front": "Fetch im Effekt", "back": "Einen Datenabruf startet man in einer Funktionskomponente üblicherweise in useEffect.", "notes": "So läuft er nach dem Render und lässt sich sauber aufräumen.", @@ -21,6 +22,7 @@ }, { "id": "loading", + "stable_id": "card-ms942h7o085iy", "front": "Ladezustand", "back": "Während der Abruf läuft, zeigt man einen Ladezustand (z. B. einen Spinner).", "notes": "Englisch: loading.", @@ -33,6 +35,7 @@ }, { "id": "error", + "stable_id": "card-ms942h7o086ne", "front": "Fehlerzustand", "back": "Schlägt der Abruf fehl, zeigt man eine Fehlermeldung statt der Daten.", "notes": "Fehler immer behandeln — Netzwerke sind unzuverlässig.", @@ -45,6 +48,7 @@ }, { "id": "success", + "stable_id": "card-ms942h7o087jb", "front": "Erfolgszustand", "back": "Bei Erfolg zeigt man die geladenen Daten an.", "notes": "Die drei Zustände loading/error/success decken den Ablauf ab.", @@ -57,6 +61,7 @@ }, { "id": "race", + "stable_id": "card-ms942h7o088lj", "front": "Race-Condition", "back": "Zwei Abrufe überholen sich: eine ältere Antwort überschreibt eine neuere und zeigt veraltete Daten.", "notes": "Passiert leicht, wenn sich ein Suchbegriff schnell ändert.", @@ -69,6 +74,7 @@ }, { "id": "abbruch", + "stable_id": "card-ms942h7o089bx", "front": "Abbruch/Ignorieren", "back": "Im Cleanup markiert man ein Ergebnis als veraltet (ignore-Flag) oder bricht mit AbortController ab.", "notes": "So überschreibt keine alte Antwort die neue.", @@ -81,6 +87,7 @@ }, { "id": "kein-mutieren", + "stable_id": "card-ms942h7o08an9", "front": "Kein direktes Mutieren", "back": "Geladene Daten setzt man über die State-Setter-Funktion, nicht durch direktes Verändern.", "notes": "Direkte Mutation löst keinen Re-Render aus.", @@ -131,7 +138,7 @@ "Gar nicht in der Komponente" ], "hint": "Nach dem Render, sauber aufräumbar.", - "id": "ex-mc-wo" + "id": "ex-mc-wo", "stable_id": "ex-ms942h7o08bwi" } }, { @@ -160,7 +167,7 @@ "Zwei Komponenten teilen sich einen Key" ], "hint": "Veraltete Daten trotz neuerer Anfrage.", - "id": "ex-mc-race" + "id": "ex-mc-race", "stable_id": "ex-ms942h7o08ct7" } }, { @@ -189,7 +196,7 @@ "Das Dependency-Array weglassen" ], "hint": "ignore-Flag oder AbortController.", - "id": "ex-mc-abbruch" + "id": "ex-mc-abbruch", "stable_id": "ex-ms942h7o08di2" } }, { @@ -218,7 +225,7 @@ "Gar nicht, useEffect setzt selbst" ], "hint": "Direkte Mutation löst keinen Re-Render aus.", - "id": "ex-mc-mutieren" + "id": "ex-mc-mutieren", "stable_id": "ex-ms942h7o08e26" } }, { @@ -246,7 +253,7 @@ "Kompiliert (compiling)" ], "hint": "Drei Zustände decken den Ablauf ab.", - "id": "ex-ms-zustaende" + "id": "ex-ms-zustaende", "stable_id": "ex-ms942h7o08fao" } }, { @@ -274,7 +281,7 @@ "Den Effekt vor dem Render ausführen" ], "hint": "Laden, Fehler, Aufräumen.", - "id": "ex-ms-robust" + "id": "ex-ms-robust", "stable_id": "ex-ms942h7o08gf1" } }, { @@ -305,7 +312,7 @@ } ], "direction": "target_to_source", - "id": "ex-match-zustand" + "id": "ex-match-zustand", "stable_id": "ex-ms942h7o08hri" } }, { @@ -329,7 +336,7 @@ ], "cloze_mode": "type", "hint": "Der Hook für Seiteneffekte.", - "id": "ex-tip-hook" + "id": "ex-tip-hook", "stable_id": "ex-ms942h7o08ij9" } } ] diff --git a/sets/de/react-fortgeschritten/lessons/03-usecontext.json b/sets/de/react-fortgeschritten/lessons/03-usecontext.json index b29bd48..4cc56b8 100644 --- a/sets/de/react-fortgeschritten/lessons/03-usecontext.json +++ b/sets/de/react-fortgeschritten/lessons/03-usecontext.json @@ -9,6 +9,7 @@ "cards": [ { "id": "usecontext", + "stable_id": "card-ms942h7o08jrl", "front": "useContext", "back": "Der Hook, der den Wert des nächstgelegenen Providers liest.", "notes": "Ersetzt tiefes Durchreichen von Props.", @@ -21,6 +22,7 @@ }, { "id": "provider", + "stable_id": "card-ms942h7o08kuw", "front": "Provider", "back": "Die Komponente, die einen Context-Wert für ihren Teilbaum bereitstellt.", "notes": "z. B. .", @@ -33,6 +35,7 @@ }, { "id": "prop-drilling", + "stable_id": "card-ms942h7o08lzf", "front": "Prop-Drilling", "back": "Props durch viele Zwischenebenen reichen, nur um sie tief unten zu nutzen.", "notes": "Genau das Problem, das Context löst.", @@ -45,6 +48,7 @@ }, { "id": "when-context", + "stable_id": "card-ms942h7o08mqf", "front": "Wann Context?", "back": "Für Daten, die viele, tief verschachtelte Komponenten brauchen (Theme, Sprache, angemeldeter Nutzer).", "notes": "Für rein lokalen State ist Context überflüssig.", @@ -57,6 +61,7 @@ }, { "id": "context-rerender", + "stable_id": "card-ms942h7o08n4y", "front": "Re-Render bei Context", "back": "Ändert sich der Context-Wert, rendern alle konsumierenden Komponenten neu.", "notes": "Deshalb den Wert bewusst gestalten.", @@ -69,6 +74,7 @@ }, { "id": "consumer", + "stable_id": "card-ms942h7o08opt", "front": "Konsument", "back": "Eine Komponente, die den Context-Wert per useContext liest.", "notes": "Sie bekommt den Wert des nächsten Providers oberhalb.", @@ -120,7 +126,7 @@ "Falsche Listen-Keys" ], "hint": "Kein Durchreichen über viele Ebenen.", - "id": "ex-mc-wogegen" + "id": "ex-mc-wogegen", "stable_id": "ex-ms942h7o08p2d" } }, { @@ -149,7 +155,7 @@ "Ein ref" ], "hint": "…Context.Provider value={...}.", - "id": "ex-mc-provider" + "id": "ex-mc-provider", "stable_id": "ex-ms942h7o08q0g" } }, { @@ -179,7 +185,7 @@ "Mit einem Provider" ], "hint": "Der Consumer-Hook.", - "id": "ex-mc-liest" + "id": "ex-mc-liest", "stable_id": "ex-ms942h7o08r3q" } }, { @@ -208,7 +214,7 @@ "Die App startet neu" ], "hint": "Konsumenten reagieren auf den neuen Wert.", - "id": "ex-mc-rerender" + "id": "ex-mc-rerender", "stable_id": "ex-ms942h7o08sfx" } }, { @@ -233,7 +239,7 @@ "Um Re-Renders generell zu verhindern" ], "hint": "Global und tief geteilt.", - "id": "ex-ms-wann" + "id": "ex-ms-wann", "stable_id": "ex-ms942h7o08to1" } }, { @@ -259,7 +265,7 @@ "Es verhindert jeden Re-Render" ], "hint": "Provider-Wert lesen, Drilling vermeiden.", - "id": "ex-ms-context" + "id": "ex-ms-context", "stable_id": "ex-ms942h7o08uiz" } }, { @@ -290,7 +296,7 @@ } ], "direction": "target_to_source", - "id": "ex-match-context" + "id": "ex-match-context", "stable_id": "ex-ms942h7o08vg0" } }, { @@ -314,7 +320,7 @@ ], "cloze_mode": "type", "hint": "Beginnt mit „use\".", - "id": "ex-tip-hook" + "id": "ex-tip-hook", "stable_id": "ex-ms942h7o08wb9" } } ] diff --git a/sets/de/react-fortgeschritten/lessons/04-useref.json b/sets/de/react-fortgeschritten/lessons/04-useref.json index fd703cb..b491ea7 100644 --- a/sets/de/react-fortgeschritten/lessons/04-useref.json +++ b/sets/de/react-fortgeschritten/lessons/04-useref.json @@ -9,6 +9,7 @@ "cards": [ { "id": "useref", + "stable_id": "card-ms942h7p08x5b", "front": "useRef", "back": "Ein Hook, der ein veränderliches Objekt mit der Eigenschaft .current liefert.", "notes": "Nützlich für DOM-Zugriff und für Werte, die keinen Re-Render auslösen sollen.", @@ -21,6 +22,7 @@ }, { "id": "dom-ref", + "stable_id": "card-ms942h7p08yll", "front": "DOM-Referenz", "back": "Über ein ref am JSX-Element greift man direkt auf den DOM-Knoten zu (z. B. Fokus setzen).", "notes": "ref={inputRef} und dann inputRef.current.focus().", @@ -33,6 +35,7 @@ }, { "id": "kein-rerender", + "stable_id": "card-ms942h7p08zxf", "front": "Kein Re-Render", "back": "Eine Änderung von ref.current löst KEINEN Re-Render aus.", "notes": "Ein häufiger Irrtum: dass ref-Änderungen neu rendern.", @@ -45,6 +48,7 @@ }, { "id": "mutable", + "stable_id": "card-ms942h7p0901l", "front": "Veränderlicher Wert", "back": "Ein ref speichert einen Wert stabil über Renders hinweg, ohne ihn anzuzeigen.", "notes": "z. B. eine Timer-ID oder der vorige Wert.", @@ -57,6 +61,7 @@ }, { "id": "current", + "stable_id": "card-ms942h7p0914o", "front": "ref.current", "back": "Über die Eigenschaft .current liest und schreibt man den Ref-Wert.", "notes": "const r = useRef(0); r.current = 5;", @@ -69,6 +74,7 @@ }, { "id": "state-vs-ref", + "stable_id": "card-ms942h7p092xw", "front": "State vs. ref", "back": "State-Änderung rendert neu und zeigt sich in der UI; ref-Änderung nicht.", "notes": "Sichtbares → State, Kulisse → ref.", @@ -120,7 +126,7 @@ "Ein erneutes Mounten" ], "hint": "Ein häufiger Irrtum ist das Gegenteil.", - "id": "ex-mc-rerender" + "id": "ex-mc-rerender", "stable_id": "ex-ms942h7p093lc" } }, { @@ -149,7 +155,7 @@ "Um Props durchzureichen" ], "hint": "z. B. Fokus setzen.", - "id": "ex-mc-dom" + "id": "ex-mc-dom", "stable_id": "ex-ms942h7p0948n" } }, { @@ -178,7 +184,7 @@ "Über ref()" ], "hint": "Die Eigenschaft heißt current.", - "id": "ex-mc-current" + "id": "ex-mc-current", "stable_id": "ex-ms942h7p095ex" } }, { @@ -207,7 +213,7 @@ "ref rendert neu, State nicht" ], "hint": "Sichtbares → State.", - "id": "ex-mc-vs" + "id": "ex-mc-vs", "stable_id": "ex-ms942h7p096ks" } }, { @@ -234,7 +240,7 @@ "Jede Änderung triggert ein Update" ], "hint": "current + kein Re-Render.", - "id": "ex-ms-useref" + "id": "ex-ms-useref", "stable_id": "ex-ms942h7p097d5" } }, { @@ -260,7 +266,7 @@ "Absichtlich einen Re-Render auslösen" ], "hint": "DOM und Kulissen-Werte.", - "id": "ex-ms-wofuer" + "id": "ex-ms-wofuer", "stable_id": "ex-ms942h7p098yt" } }, { @@ -291,7 +297,7 @@ } ], "direction": "target_to_source", - "id": "ex-match-refstate" + "id": "ex-match-refstate", "stable_id": "ex-ms942h7p099om" } }, { @@ -315,7 +321,7 @@ ], "cloze_mode": "type", "hint": "Beginnt mit „use\".", - "id": "ex-tip-name" + "id": "ex-tip-name", "stable_id": "ex-ms942h7p09aa8" } } ] diff --git a/sets/de/react-fortgeschritten/lessons/05-performance.json b/sets/de/react-fortgeschritten/lessons/05-performance.json index c397d14..faa0768 100644 --- a/sets/de/react-fortgeschritten/lessons/05-performance.json +++ b/sets/de/react-fortgeschritten/lessons/05-performance.json @@ -9,6 +9,7 @@ "cards": [ { "id": "rerender-ausloeser", + "stable_id": "card-ms942h7q09bt5", "front": "Re-Render-Auslöser", "back": "Eine Komponente rendert neu, wenn sich ihr State oder ihre Props ändern (oder ihr Elternteil neu rendert).", "notes": "Eine ref.current-Änderung gehört NICHT dazu.", @@ -21,6 +22,7 @@ }, { "id": "usememo", + "stable_id": "card-ms942h7q09c5x", "front": "useMemo", "back": "Cached das ERGEBNIS einer teuren Berechnung zwischen Renders, solange die Dependencies gleich bleiben.", "notes": "Nur für wirklich teure Berechnungen sinnvoll.", @@ -33,6 +35,7 @@ }, { "id": "usecallback", + "stable_id": "card-ms942h7q09dtw", "front": "useCallback", "back": "Cached eine FUNKTIONSREFERENZ zwischen Renders, solange die Dependencies gleich bleiben.", "notes": "Nützlich, wenn ein memoisiertes Kind eine stabile Funktion braucht.", @@ -45,6 +48,7 @@ }, { "id": "react-memo", + "stable_id": "card-ms942h7q09em5", "front": "React.memo", "back": "Umhüllt eine Komponente, die ihren Re-Render überspringt, solange ihre Props gleich bleiben.", "notes": "Wirkt nur mit stabilen Props zusammen.", @@ -57,6 +61,7 @@ }, { "id": "premature", + "stable_id": "card-ms942h7q09fcg", "front": "Voreilige Optimierung", "back": "Alles zu memoisieren bringt oft nichts und macht den Code komplizierter.", "notes": "Erst messen, dann gezielt optimieren.", @@ -69,6 +74,7 @@ }, { "id": "props-stabil", + "stable_id": "card-ms942h7q09g7b", "front": "Stabile Props", "back": "Damit React.memo greift, müssen die übergebenen Props (auch Funktionen) stabil bleiben.", "notes": "Hier hilft useCallback.", @@ -119,7 +125,7 @@ "Ein Kommentar im Code" ], "hint": "State/Props (oder das Elternteil).", - "id": "ex-mc-ausloeser" + "id": "ex-mc-ausloeser", "stable_id": "ex-ms942h7q09hnp" } }, { @@ -148,7 +154,7 @@ "Den DOM-Knoten" ], "hint": "Ergebnis, nicht Funktion.", - "id": "ex-mc-usememo" + "id": "ex-mc-usememo", "stable_id": "ex-ms942h7q09ixg" } }, { @@ -177,7 +183,7 @@ "Den State" ], "hint": "Funktion, nicht Ergebnis.", - "id": "ex-mc-usecallback" + "id": "ex-mc-usecallback", "stable_id": "ex-ms942h7q09jzk" } }, { @@ -206,7 +212,7 @@ "Es lädt Daten" ], "hint": "Gleiche Props → kein Re-Render.", - "id": "ex-mc-reactmemo" + "id": "ex-mc-reactmemo", "stable_id": "ex-ms942h7q09kh7" } }, { @@ -234,7 +240,7 @@ "Man sollte grundsätzlich alles memoisieren" ], "hint": "Zwei cachen, eine Übertreibung meiden.", - "id": "ex-ms-memo" + "id": "ex-ms-memo", "stable_id": "ex-ms942h7q09l5k" } }, { @@ -260,7 +266,7 @@ "Um jeden Re-Render zu verbieten" ], "hint": "Gezielt, nicht pauschal.", - "id": "ex-ms-wann" + "id": "ex-ms-wann", "stable_id": "ex-ms942h7q09mjx" } }, { @@ -291,7 +297,7 @@ } ], "direction": "target_to_source", - "id": "ex-match-perf" + "id": "ex-match-perf", "stable_id": "ex-ms942h7q09na7" } }, { @@ -315,7 +321,7 @@ ], "cloze_mode": "type", "hint": "Beginnt mit „use\", cachet einen Wert.", - "id": "ex-tip-memo" + "id": "ex-tip-memo", "stable_id": "ex-ms942h7q09oac" } } ] diff --git a/sets/de/react-fortgeschritten/lessons/06-listen-keys.json b/sets/de/react-fortgeschritten/lessons/06-listen-keys.json index 9457d09..bb5c938 100644 --- a/sets/de/react-fortgeschritten/lessons/06-listen-keys.json +++ b/sets/de/react-fortgeschritten/lessons/06-listen-keys.json @@ -9,6 +9,7 @@ "cards": [ { "id": "key", + "stable_id": "card-ms942h7q09pf5", "front": "Key", "back": "Ein Attribut an Listenelementen, mit dem React ein Element über Renders hinweg wiedererkennt.", "notes": "
  • .", @@ -21,6 +22,7 @@ }, { "id": "stabil-eindeutig", + "stable_id": "card-ms942h7q09qpd", "front": "Stabil und eindeutig", "back": "Ein guter Key ist stabil (ändert sich nicht) und eindeutig unter den Geschwistern.", "notes": "Meist eine ID aus den Daten.", @@ -33,6 +35,7 @@ }, { "id": "index-problem", + "stable_id": "card-ms942h7q09rd8", "front": "Index als Key", "back": "Der Array-Index als Key führt bei Umsortieren, Einfügen oder Löschen zu falscher State-Zuordnung.", "notes": "Ein Irrtum ist, der Index sei immer sicher.", @@ -45,6 +48,7 @@ }, { "id": "nur-geschwister", + "stable_id": "card-ms942h7q09s16", "front": "Nur unter Geschwistern", "back": "Keys müssen nur innerhalb derselben Liste eindeutig sein, nicht global in der App.", "notes": "Zwei verschiedene Listen dürfen denselben Key nutzen.", @@ -57,6 +61,7 @@ }, { "id": "reconciliation", + "stable_id": "card-ms942h7q09t2h", "front": "Zuordnung (Reconciliation)", "back": "React nutzt Keys, um alte und neue Listenelemente einander zuzuordnen und nur das Nötige zu ändern.", "notes": "Falsche Keys führen zu falschem oder unnötigem Update.", @@ -69,6 +74,7 @@ }, { "id": "index-ok", + "stable_id": "card-ms942h7q09uuq", "front": "Index vertretbar", "back": "Der Index ist nur vertretbar, wenn die Liste statisch ist und sich Reihenfolge/Anzahl nie ändern.", "notes": "Sobald sortiert oder gelöscht wird: eine echte ID nehmen.", @@ -120,7 +126,7 @@ "Damit die Liste sortiert wird" ], "hint": "Zuordnung alt ↔ neu.", - "id": "ex-mc-wozu" + "id": "ex-mc-wozu", "stable_id": "ex-ms942h7q09v5s" } }, { @@ -149,7 +155,7 @@ "Der Index ist technisch verboten" ], "hint": "Klassischer Irrtum: „immer sicher\".", - "id": "ex-mc-index" + "id": "ex-mc-index", "stable_id": "ex-ms942h7q09w6l" } }, { @@ -178,7 +184,7 @@ "Der angezeigte Text" ], "hint": "Stabil + eindeutig.", - "id": "ex-mc-gut" + "id": "ex-mc-gut", "stable_id": "ex-ms942h7q09xsx" } }, { @@ -207,7 +213,7 @@ "Nur beim ersten Render" ], "hint": "Nicht global.", - "id": "ex-mc-wo" + "id": "ex-mc-wo", "stable_id": "ex-ms942h7q09yj2" } }, { @@ -234,7 +240,7 @@ "Keys müssen global eindeutig sein" ], "hint": "Stabil, zuordnend, nur unter Geschwistern.", - "id": "ex-ms-keys" + "id": "ex-ms-keys", "stable_id": "ex-ms942h7q09z0n" } }, { @@ -259,7 +265,7 @@ "Wenn Einträge gelöscht werden" ], "hint": "Nur bei unveränderlichen Listen.", - "id": "ex-ms-index-ok" + "id": "ex-ms-index-ok", "stable_id": "ex-ms942h7q0a0ze" } }, { @@ -290,7 +296,7 @@ } ], "direction": "target_to_source", - "id": "ex-match-keys" + "id": "ex-match-keys", "stable_id": "ex-ms942h7q0a1os" } }, { @@ -315,7 +321,7 @@ ], "cloze_mode": "type", "hint": "Englisch für 'Schlüssel'.", - "id": "ex-tip-key" + "id": "ex-tip-key", "stable_id": "ex-ms942h7q0a22y" } } ] diff --git a/sets/de/react-fortgeschritten/lessons/07-formulare.json b/sets/de/react-fortgeschritten/lessons/07-formulare.json index 5db6fe4..7d2a067 100644 --- a/sets/de/react-fortgeschritten/lessons/07-formulare.json +++ b/sets/de/react-fortgeschritten/lessons/07-formulare.json @@ -9,6 +9,7 @@ "cards": [ { "id": "controlled", + "stable_id": "card-ms942h7r0a3cp", "front": "Controlled Input", "back": "Ein Eingabefeld, dessen Wert aus dem State kommt (value) und über onChange den State aktualisiert.", "notes": "Der State ist die einzige Quelle der Wahrheit.", @@ -21,6 +22,7 @@ }, { "id": "uncontrolled", + "stable_id": "card-ms942h7r0a4ps", "front": "Uncontrolled Input", "back": "Ein Eingabefeld, dessen Wert im DOM lebt und bei Bedarf per ref ausgelesen wird.", "notes": "Oft mit defaultValue als Startwert.", @@ -33,6 +35,7 @@ }, { "id": "value-onchange", + "stable_id": "card-ms942h7r0a5d0", "front": "value + onChange", "back": "Das Paar, das einen Input controlled macht: value bindet an den State, onChange schreibt zurück.", "notes": "Ohne onChange wäre das Feld schreibgeschützt.", @@ -45,6 +48,7 @@ }, { "id": "single-source", + "stable_id": "card-ms942h7r0a6k6", "front": "Einzige Quelle der Wahrheit", "back": "Bei controlled Inputs kennt der State den aktuellen Wert — genau eine Stelle, nicht das DOM zusätzlich.", "notes": "Erleichtert Validierung und abhängige Felder.", @@ -57,6 +61,7 @@ }, { "id": "defaultvalue", + "stable_id": "card-ms942h7r0a7iq", "front": "defaultValue", "back": "Setzt bei einem uncontrolled Input nur den Startwert; danach lebt der Wert im DOM.", "notes": "Nicht mit value verwechseln (das bindet dauerhaft).", @@ -69,6 +74,7 @@ }, { "id": "ref-auslesen", + "stable_id": "card-ms942h7r0a8gs", "front": "Auslesen per ref", "back": "Den Wert eines uncontrolled Inputs holt man bei Bedarf über ein ref (z. B. beim Absenden).", "notes": "z. B. inputRef.current.value.", @@ -120,7 +126,7 @@ "Er hat keinen onChange-Handler" ], "hint": "State steuert den Wert.", - "id": "ex-mc-controlled" + "id": "ex-mc-controlled", "stable_id": "ex-ms942h7r0a9x8" } }, { @@ -150,7 +156,7 @@ "Er rendert bei jedem Tastendruck neu" ], "hint": "Wert im DOM, Zugriff per ref.", - "id": "ex-mc-uncontrolled" + "id": "ex-mc-uncontrolled", "stable_id": "ex-ms942h7r0aajj" } }, { @@ -179,7 +185,7 @@ "Ein ref allein" ], "hint": "Binden + zurückschreiben.", - "id": "ex-mc-macht" + "id": "ex-mc-macht", "stable_id": "ex-ms942h7r0abkr" } }, { @@ -208,7 +214,7 @@ "Mit dem Dependency-Array" ], "hint": "z. B. inputRef.current.value.", - "id": "ex-mc-auslesen" + "id": "ex-mc-auslesen", "stable_id": "ex-ms942h7r0acf4" } }, { @@ -236,7 +242,7 @@ "Man liest ihn nur per ref aus" ], "hint": "State steuert, State weiß Bescheid.", - "id": "ex-ms-controlled" + "id": "ex-ms-controlled", "stable_id": "ex-ms942h7r0adsz" } }, { @@ -264,7 +270,7 @@ "onChange ist bei jedem Tastendruck nötig" ], "hint": "DOM-Wert, ref, defaultValue.", - "id": "ex-ms-uncontrolled" + "id": "ex-ms-uncontrolled", "stable_id": "ex-ms942h7r0aen9" } }, { @@ -295,7 +301,7 @@ } ], "direction": "target_to_source", - "id": "ex-match-form" + "id": "ex-match-form", "stable_id": "ex-ms942h7r0af4a" } }, { @@ -319,7 +325,7 @@ ], "cloze_mode": "type", "hint": "Gegenteil von „uncontrolled\".", - "id": "ex-tip-controlled" + "id": "ex-tip-controlled", "stable_id": "ex-ms942h7r0agwt" } } ] diff --git a/sets/de/react-fortgeschritten/lessons/08-komposition-patterns.json b/sets/de/react-fortgeschritten/lessons/08-komposition-patterns.json index ae48f84..78e52cb 100644 --- a/sets/de/react-fortgeschritten/lessons/08-komposition-patterns.json +++ b/sets/de/react-fortgeschritten/lessons/08-komposition-patterns.json @@ -9,6 +9,7 @@ "cards": [ { "id": "children", + "stable_id": "card-ms942h7s0ahol", "front": "children-Prop", "back": "Die besondere Prop, über die eine Komponente den zwischen ihren Tags verschachtelten Inhalt erhält.", "notes": "Grundlage der Komposition: Inhalt.", @@ -21,6 +22,7 @@ }, { "id": "lifting", + "stable_id": "card-ms942h7s0aito", "front": "Lifting State Up", "back": "Gemeinsamen State in die nächste gemeinsame Elternkomponente verschieben, wenn mehrere Kinder ihn brauchen.", "notes": "So gibt es eine einzige Quelle der Wahrheit.", @@ -33,6 +35,7 @@ }, { "id": "custom-hook", + "stable_id": "card-ms942h7s0ajvj", "front": "Custom Hook", "back": "Eine Funktion, die mit „use\" beginnt und wiederverwendbare Hook-Logik bündelt.", "notes": "Sie darf andere Hooks aufrufen und rendert selbst keine UI.", @@ -45,6 +48,7 @@ }, { "id": "error-boundary", + "stable_id": "card-ms942h7s0ak37", "front": "Error Boundary", "back": "Ein Baustein, der Render-Fehler in seinem Teilbaum abfängt und einen Fallback statt eines Absturzes zeigt.", "notes": "Konzeptuell: „Auffangnetz\" für Fehler beim Rendern.", @@ -57,6 +61,7 @@ }, { "id": "komposition", + "stable_id": "card-ms942h7s0alky", "front": "Komposition", "back": "UI aus kleinen Bausteinen zusammensetzen — Inhalte per children verschachteln statt alles zu vererben.", "notes": "React bevorzugt Komposition.", @@ -69,6 +74,7 @@ }, { "id": "use-praefix", + "stable_id": "card-ms942h7s0ambk", "front": "use-Präfix", "back": "Custom Hooks beginnen per Konvention mit „use\", damit React die Hook-Regeln prüfen kann.", "notes": "z. B. useOnlineStatus, useFormField.", @@ -81,6 +87,7 @@ }, { "id": "single-truth", + "stable_id": "card-ms942h7s0anbu", "front": "Eine Quelle der Wahrheit", "back": "Geteilter State lebt an genau einer Stelle (dem gemeinsamen Elternteil), nicht doppelt in mehreren Kindern.", "notes": "Verhindert widersprüchliche Zustände.", @@ -132,7 +139,7 @@ "Daten zu laden" ], "hint": "Inhalt.", - "id": "ex-mc-children" + "id": "ex-mc-children", "stable_id": "ex-ms942h7s0aowz" } }, { @@ -162,7 +169,7 @@ "State ganz vermeiden" ], "hint": "Eine Quelle der Wahrheit.", - "id": "ex-mc-lifting" + "id": "ex-mc-lifting", "stable_id": "ex-ms942h7s0apz8" } }, { @@ -192,7 +199,7 @@ "Ein Ersatz für den Provider" ], "hint": "Beginnt mit „use\".", - "id": "ex-mc-customhook" + "id": "ex-mc-customhook", "stable_id": "ex-ms942h7s0aqws" } }, { @@ -221,7 +228,7 @@ "State global speichern" ], "hint": "Auffangnetz beim Rendern.", - "id": "ex-mc-boundary" + "id": "ex-mc-boundary", "stable_id": "ex-ms942h7s0arei" } }, { @@ -248,7 +255,7 @@ "Sie ersetzen Komponenten vollständig" ], "hint": "use-Präfix, Logik, Hooks erlaubt.", - "id": "ex-ms-customhook" + "id": "ex-ms-customhook", "stable_id": "ex-ms942h7s0astt" } }, { @@ -274,7 +281,7 @@ "Um jeden Re-Render zu verbieten" ], "hint": "Geteilter Bedarf, eine Quelle.", - "id": "ex-ms-lifting" + "id": "ex-ms-lifting", "stable_id": "ex-ms942h7s0at8g" } }, { @@ -310,7 +317,7 @@ } ], "direction": "target_to_source", - "id": "ex-match-patterns" + "id": "ex-match-patterns", "stable_id": "ex-ms942h7s0auo9" } }, { @@ -336,7 +343,7 @@ ], "cloze_mode": "type", "hint": "Englischer Fachbegriff.", - "id": "ex-tip-hook" + "id": "ex-tip-hook", "stable_id": "ex-ms942h7s0av1x" } } ] diff --git a/sets/de/react-grundlagen/lessons/01-was-ist-react.json b/sets/de/react-grundlagen/lessons/01-was-ist-react.json index 1217b0e..cb59bdc 100644 --- a/sets/de/react-grundlagen/lessons/01-was-ist-react.json +++ b/sets/de/react-grundlagen/lessons/01-was-ist-react.json @@ -9,6 +9,7 @@ "cards": [ { "id": "card-react", + "stable_id": "card-ms942h7s0awbq", "front": "React", "back": "Eine JavaScript-Library zum Bauen von Benutzeroberflächen aus wiederverwendbaren Komponenten.", "notes": "React kümmert sich um die UI-Schicht. Routing, Datenabruf usw. ergänzt man mit weiteren Bibliotheken.", @@ -21,6 +22,7 @@ }, { "id": "card-library", + "stable_id": "card-ms942h7s0axo4", "front": "Library statt Framework", "back": "React ist eine Library (Bibliothek), kein vollständiges Framework – es deckt vor allem die Benutzeroberfläche ab.", "notes": "Eine Library ruft man aus dem eigenen Code auf; ein Framework gibt dagegen die gesamte Struktur vor.", @@ -33,6 +35,7 @@ }, { "id": "card-komponente", + "stable_id": "card-ms942h7s0ayez", "front": "Komponente", "back": "Ein eigenständiger, wiederverwendbarer Baustein der Benutzeroberfläche.", "notes": "Komponenten lassen sich verschachteln und kombinieren – daraus entsteht die ganze Oberfläche.", @@ -44,6 +47,7 @@ }, { "id": "card-funktionskomponente", + "stable_id": "card-ms942h7s0azrq", "front": "Funktionskomponente", "back": "Eine JavaScript-Funktion, die JSX zurückgibt und damit ein Stück Oberfläche beschreibt.", "notes": "Moderne React-Komponenten sind Funktionen. Sie beginnen mit einem Großbuchstaben (z. B. Profil).", @@ -57,6 +61,7 @@ }, { "id": "card-deklarativ", + "stable_id": "card-ms942h7s0b02u", "front": "Deklarativ", "back": "Du beschreibst, wie die UI bei einem bestimmten Zustand aussehen soll; React sorgt für die nötigen DOM-Änderungen.", "notes": "Gegenstück: imperativ, wo man jeden DOM-Schritt selbst anweist.", @@ -68,6 +73,7 @@ }, { "id": "card-komponentenbasiert", + "stable_id": "card-ms942h7s0b1px", "front": "Komponentenbasiert", "back": "Oberflächen werden aus kleinen, kombinierbaren Komponenten zusammengesetzt.", "notes": "Eine Seite besteht typischerweise aus vielen verschachtelten Komponenten.", @@ -80,6 +86,7 @@ }, { "id": "card-rueckgabe", + "stable_id": "card-ms942h7s0b27o", "front": "Was gibt eine Funktionskomponente zurück?", "back": "JSX – eine Beschreibung der anzuzeigenden Oberfläche.", "notes": "Eine Komponente gibt genau ein Wurzelelement zurück (ggf. ein Fragment <>…).", @@ -110,6 +117,7 @@ "title": "Was ist React?", "exercise": { "id": "ex-mc-was-ist-react", + "stable_id": "ex-ms942h7s0b3rc", "type": "cloze", "prompt": "Was beschreibt React am besten?", "card_ids": [ @@ -139,6 +147,7 @@ "title": "Was ist eine Komponente?", "exercise": { "id": "ex-mc-komponente", + "stable_id": "ex-ms942h7s0b46c", "type": "cloze", "prompt": "Was ist eine Komponente in React?", "card_ids": [ @@ -168,6 +177,7 @@ "title": "Rückgabewert einer Funktionskomponente", "exercise": { "id": "ex-mc-rueckgabe", + "stable_id": "ex-ms942h7s0b51c", "type": "cloze", "prompt": "Was gibt eine Funktionskomponente typischerweise zurück?", "card_ids": [ @@ -197,6 +207,7 @@ "title": "Deklaratives UI-Modell", "exercise": { "id": "ex-mc-deklarativ", + "stable_id": "ex-ms942h7s0b6l0", "type": "cloze", "prompt": "Was bedeutet es, dass React „deklarativ“ arbeitet?", "card_ids": [ @@ -225,6 +236,7 @@ "title": "Code lesen: gültige Komponente", "exercise": { "id": "ex-mc-code-komponente", + "stable_id": "ex-ms942h7s0b78o", "type": "cloze", "prompt": "Welche dieser Funktionen ist eine gültige React-Funktionskomponente?", "card_ids": [ @@ -253,6 +265,7 @@ "title": "Lückentext: Library", "exercise": { "id": "ex-cloze-library", + "stable_id": "ex-ms942h7s0b8n9", "type": "cloze", "prompt": "Ergänze das fehlende Wort.", "card_ids": [ @@ -278,6 +291,7 @@ "title": "Begriffe zuordnen", "exercise": { "id": "ex-match-begriffe", + "stable_id": "ex-ms942h7s0b9l2", "type": "matching", "prompt": "Ordne jedem Begriff die passende Kurzbeschreibung zu.", "card_ids": [ diff --git a/sets/de/react-grundlagen/lessons/02-jsx.json b/sets/de/react-grundlagen/lessons/02-jsx.json index ff67640..0bc278a 100644 --- a/sets/de/react-grundlagen/lessons/02-jsx.json +++ b/sets/de/react-grundlagen/lessons/02-jsx.json @@ -9,6 +9,7 @@ "cards": [ { "id": "card-jsx", + "stable_id": "card-ms942h7s0ba8w", "front": "JSX", "back": "Eine Syntaxerweiterung von JavaScript, mit der man UI-Struktur HTML-ähnlich im Code beschreibt.", "notes": "JSX wird vor der Ausführung zu JavaScript-Aufrufen kompiliert (z. B. React.createElement).", @@ -21,6 +22,7 @@ }, { "id": "card-kein-html", + "stable_id": "card-ms942h7s0bb4h", "front": "Ist JSX HTML?", "back": "Nein. JSX sieht HTML ähnlich, ist aber JavaScript und wird zu JavaScript-Code kompiliert.", "notes": "Deshalb gelten JavaScript-Regeln, z. B. className statt class.", @@ -33,6 +35,7 @@ }, { "id": "card-ausdruck", + "stable_id": "card-ms942h7s0bc0k", "front": "Ausdrücke in {}", "back": "In geschweiften Klammern { } bettet man beliebige JavaScript-Ausdrücke in JSX ein.", "notes": "Beispiel:

    Hallo {name}

    setzt den Wert der Variablen name ein.", @@ -47,6 +50,7 @@ }, { "id": "card-classname", + "stable_id": "card-ms942h7s0bdiu", "front": "className statt class", "back": "Weil JSX JavaScript ist und class dort ein reserviertes Wort ist, schreibt man className.", "notes": "Ähnlich: htmlFor statt for. Attribute werden in camelCase geschrieben (z. B. onClick).", @@ -59,6 +63,7 @@ }, { "id": "card-ein-wurzelelement", + "stable_id": "card-ms942h7s0be85", "front": "Ein Wurzelelement", "back": "JSX, das eine Komponente zurückgibt, muss genau ein umschließendes Wurzelelement haben.", "notes": "Mehrere Elemente umschließt man mit einem Tag oder einem Fragment <>….", @@ -71,6 +76,7 @@ }, { "id": "card-curly-vs-string", + "stable_id": "card-ms942h7s0bfc5", "front": "{} gegen Anführungszeichen", "back": "{ } werten einen JavaScript-Ausdruck aus; Anführungszeichen liefern einen festen Text-String.", "notes": "title=\"Hallo\" ist fester Text, title={gruss} setzt den Wert der Variablen gruss ein.", @@ -101,6 +107,7 @@ "title": "Was ist JSX?", "exercise": { "id": "ex-mc-was-ist-jsx", + "stable_id": "ex-ms942h7s0bgfl", "type": "cloze", "prompt": "Was ist JSX?", "card_ids": [ @@ -130,6 +137,7 @@ "title": "JSX und HTML", "exercise": { "id": "ex-mc-jsx-kein-html", + "stable_id": "ex-ms942h7s0bhsx", "type": "cloze", "prompt": "Welche Aussage über JSX und HTML ist korrekt?", "card_ids": [ @@ -159,6 +167,7 @@ "title": "Ausdrücke einbetten", "exercise": { "id": "ex-mc-ausdruck", + "stable_id": "ex-ms942h7s0biyq", "type": "cloze", "prompt": "Wie bettet man in JSX den Wert einer Variablen name in den Text ein?", "card_ids": [ @@ -188,6 +197,7 @@ "title": "Code lesen: Was wird angezeigt?", "exercise": { "id": "ex-mc-code-ausgabe", + "stable_id": "ex-ms942h7s0bju4", "type": "cloze", "prompt": "Was zeigt dieses JSX an? const n = 3; const el =

    Summe: {n + 2}

    ;", "card_ids": [ @@ -216,6 +226,7 @@ "title": "Attribute in JSX", "exercise": { "id": "ex-mc-classname", + "stable_id": "ex-ms942h7s0bkyf", "type": "cloze", "prompt": "Wie gibt man in JSX einem Element eine CSS-Klasse?", "card_ids": [ @@ -244,6 +255,7 @@ "title": "Lückentext: Klassenattribut", "exercise": { "id": "ex-cloze-curly", + "stable_id": "ex-ms942h7s0bl76", "type": "cloze", "prompt": "Ergänze das in JSX korrekte Attribut für eine CSS-Klasse.", "card_ids": [ @@ -268,6 +280,7 @@ "title": "Begriffe zuordnen", "exercise": { "id": "ex-match-begriffe", + "stable_id": "ex-ms942h7s0bmf1", "type": "matching", "prompt": "Ordne jedem Begriff die passende Kurzbeschreibung zu.", "card_ids": [ diff --git a/sets/de/react-grundlagen/lessons/03-props.json b/sets/de/react-grundlagen/lessons/03-props.json index 11d6552..e2ab4c6 100644 --- a/sets/de/react-grundlagen/lessons/03-props.json +++ b/sets/de/react-grundlagen/lessons/03-props.json @@ -9,6 +9,7 @@ "cards": [ { "id": "card-props", + "stable_id": "card-ms942h7t0bn97", "front": "Props", "back": "Daten, die eine Elternkomponente an eine Kindkomponente übergibt, um sie zu konfigurieren.", "notes": "Kurz für „properties“. Werden wie Attribute geschrieben: .", @@ -23,6 +24,7 @@ }, { "id": "card-readonly", + "stable_id": "card-ms942h7t0bot4", "front": "Props sind read-only", "back": "Eine Komponente darf ihre eigenen Props nicht verändern; sie liest sie nur.", "notes": "Props gehören der Elternkomponente. Ändern lässt sich der Wert nur dort, wo er entsteht.", @@ -35,6 +37,7 @@ }, { "id": "card-unidirektional", + "stable_id": "card-ms942h7t0bpgn", "front": "Unidirektionaler Datenfluss", "back": "Daten fließen in React in eine Richtung: von der Eltern- zur Kindkomponente.", "notes": "Auch „one-way data flow“. Das macht nachvollziehbar, woher ein Wert kommt.", @@ -47,6 +50,7 @@ }, { "id": "card-props-vs-state", + "stable_id": "card-ms942h7t0bqyl", "front": "Props gegen State", "back": "Props kommen von außen (vom Elternteil) und sind read-only; State gehört der Komponente selbst und ist veränderbar.", "notes": "Faustregel: Bekommt die Komponente den Wert übergeben? -> Props. Verwaltet sie ihn selbst? -> State.", @@ -60,6 +64,7 @@ }, { "id": "card-props-uebergeben", + "stable_id": "card-ms942h7t0brpd", "front": "Props übergeben", "back": "Beim Verwenden der Komponente schreibt man Props wie Attribute: .", "notes": "Werte aus Variablen übergibt man mit { }: name={benutzer}.", @@ -74,6 +79,7 @@ }, { "id": "card-children", + "stable_id": "card-ms942h7t0bspn", "front": "props.children", "back": "Der Inhalt zwischen Öffnungs- und Schluss-Tag einer Komponente steht im Kind als props.children bereit.", "notes": "Ermöglicht Komposition: Inhalt reicht „Inhalt“ als children hinein.", @@ -104,6 +110,7 @@ "title": "Was sind Props?", "exercise": { "id": "ex-mc-was-sind-props", + "stable_id": "ex-ms942h7t0btfh", "type": "cloze", "prompt": "Wofür dienen Props in React?", "card_ids": [ @@ -133,6 +140,7 @@ "title": "Darf ein Kind seine Props ändern?", "exercise": { "id": "ex-mc-readonly", + "stable_id": "ex-ms942h7t0bufc", "type": "cloze", "prompt": "Was gilt für eine Komponente in Bezug auf ihre eigenen Props?", "card_ids": [ @@ -161,6 +169,7 @@ "title": "Richtung des Datenflusses", "exercise": { "id": "ex-mc-datenfluss", + "stable_id": "ex-ms942h7t0bv1i", "type": "cloze", "prompt": "In welche Richtung fließen Daten beim unidirektionalen Datenfluss?", "card_ids": [ @@ -189,6 +198,7 @@ "title": "Props oder State?", "exercise": { "id": "ex-mc-props-vs-state", + "stable_id": "ex-ms942h7t0bwpb", "type": "cloze", "prompt": "Worin unterscheiden sich Props und State?", "card_ids": [ @@ -217,6 +227,7 @@ "title": "Code lesen: Props auslesen", "exercise": { "id": "ex-mc-code-props", + "stable_id": "ex-ms942h7t0bxly", "type": "cloze", "prompt": "Was zeigt mit function Profil(props) { return

    Hallo {props.name}

    ; } an?", "card_ids": [ @@ -246,6 +257,7 @@ "title": "Lückentext: Eigenschaft von Props", "exercise": { "id": "ex-cloze-readonly", + "stable_id": "ex-ms942h7t0byf1", "type": "cloze", "prompt": "Ergänze das englische Fachwort: Eine Komponente darf ihre Props nicht ändern – sie sind ___.", "card_ids": [ @@ -271,6 +283,7 @@ "title": "Begriffe zuordnen", "exercise": { "id": "ex-match-begriffe", + "stable_id": "ex-ms942h7t0bzcb", "type": "matching", "prompt": "Ordne jedem Begriff die passende Kurzbeschreibung zu.", "card_ids": [ diff --git a/sets/de/react-grundlagen/lessons/04-state-usestate.json b/sets/de/react-grundlagen/lessons/04-state-usestate.json index 733da52..4cf2ac5 100644 --- a/sets/de/react-grundlagen/lessons/04-state-usestate.json +++ b/sets/de/react-grundlagen/lessons/04-state-usestate.json @@ -9,6 +9,7 @@ "cards": [ { "id": "card-state", + "stable_id": "card-ms942h7u0c06r", "front": "State", "back": "Veränderliche Daten, die eine Komponente selbst verwaltet und die ihre Darstellung beeinflussen.", "notes": "Ändert sich der State, rendert React die Komponente neu.", @@ -21,6 +22,7 @@ }, { "id": "card-usestate", + "stable_id": "card-ms942h7u0c1ks", "front": "useState", "back": "Der Hook, mit dem eine Funktionskomponente lokalen State erhält.", "notes": "Gibt ein Paar zurück: den aktuellen Wert und eine Setter-Funktion zum Ändern.", @@ -35,6 +37,7 @@ }, { "id": "card-setter", + "stable_id": "card-ms942h7u0c2wp", "front": "Setter-Funktion", "back": "Die von useState gelieferte Funktion (z. B. setZaehler), mit der man den State aktualisiert.", "notes": "Nur über den Setter ändern – das teilt React mit, dass neu gerendert werden muss.", @@ -49,6 +52,7 @@ }, { "id": "card-keine-mutation", + "stable_id": "card-ms942h7u0c3x3", "front": "State nicht direkt mutieren", "back": "Man verändert State nicht direkt (z. B. zaehler++ oder array.push), sondern immer über die Setter-Funktion.", "notes": "Direktes Mutieren löst kein Re-Render aus; React bemerkt die Änderung nicht.", @@ -61,6 +65,7 @@ }, { "id": "card-rerender", + "stable_id": "card-ms942h7u0c4q8", "front": "State-Änderung -> Re-Render", "back": "Ruft man die Setter-Funktion auf, plant React ein erneutes Rendern der Komponente mit dem neuen Wert.", "notes": "So bleibt die Anzeige immer im Einklang mit dem aktuellen State.", @@ -73,6 +78,7 @@ }, { "id": "card-initialwert", + "stable_id": "card-ms942h7u0c54c", "front": "Anfangswert von useState", "back": "Das Argument von useState(...) ist der Startwert des States beim ersten Rendern.", "notes": "useState(0) startet bei 0, useState('') bei einem leeren String.", @@ -103,6 +109,7 @@ "title": "Was ist State?", "exercise": { "id": "ex-mc-was-ist-state", + "stable_id": "ex-ms942h7u0c6n1", "type": "cloze", "prompt": "Was ist State in React?", "card_ids": [ @@ -131,6 +138,7 @@ "title": "Wofür useState?", "exercise": { "id": "ex-mc-usestate-zweck", + "stable_id": "ex-ms942h7u0c71c", "type": "cloze", "prompt": "Welcher Hook verwaltet lokalen State in einer Funktionskomponente?", "card_ids": [ @@ -159,6 +167,7 @@ "title": "Rückgabe von useState", "exercise": { "id": "ex-mc-rueckgabe-usestate", + "stable_id": "ex-ms942h7u0c84o", "type": "cloze", "prompt": "Was liefert const [wert, setWert] = useState(0); zurück?", "card_ids": [ @@ -189,6 +198,7 @@ "title": "State korrekt ändern", "exercise": { "id": "ex-mc-keine-mutation", + "stable_id": "ex-ms942h7u0c90t", "type": "cloze", "prompt": "Du hast const [zaehler, setZaehler] = useState(0);. Wie erhöhst du den Zähler korrekt um 1?", "card_ids": [ @@ -218,6 +228,7 @@ "title": "Warum die Setter-Funktion?", "exercise": { "id": "ex-mc-warum-setter", + "stable_id": "ex-ms942h7u0caw2", "type": "cloze", "prompt": "Warum darf man State nicht direkt mutieren, sondern muss die Setter-Funktion nutzen?", "card_ids": [ @@ -247,6 +258,7 @@ "title": "Lückentext: State-Hook", "exercise": { "id": "ex-cloze-usestate", + "stable_id": "ex-ms942h7u0cbwi", "type": "cloze", "prompt": "Ergänze den Hook, der einer Funktionskomponente lokalen State gibt.", "card_ids": [ @@ -270,6 +282,7 @@ "title": "Begriffe zuordnen", "exercise": { "id": "ex-match-begriffe", + "stable_id": "ex-ms942h7u0cci7", "type": "matching", "prompt": "Ordne jedem Begriff die passende Kurzbeschreibung zu.", "card_ids": [ diff --git a/sets/de/react-grundlagen/lessons/05-events-und-grundprinzipien.json b/sets/de/react-grundlagen/lessons/05-events-und-grundprinzipien.json index b417a4d..6085eb1 100644 --- a/sets/de/react-grundlagen/lessons/05-events-und-grundprinzipien.json +++ b/sets/de/react-grundlagen/lessons/05-events-und-grundprinzipien.json @@ -9,6 +9,7 @@ "cards": [ { "id": "card-onclick", + "stable_id": "card-ms942h7v0cdo6", "front": "onClick", "back": "Das JSX-Attribut (in camelCase), mit dem man eine Funktion auf einen Klick reagieren lässt.", "notes": "React-Event-Attribute werden in camelCase geschrieben: onClick, onChange, onSubmit.", @@ -23,6 +24,7 @@ }, { "id": "card-handler-referenz", + "stable_id": "card-ms942h7v0ceto", "front": "Handler als Funktionsreferenz", "back": "Man übergibt die Funktion selbst (onClick={handleKlick}), nicht ihr Ergebnis (onClick={handleKlick()}).", "notes": "Mit Klammern würde die Funktion sofort beim Rendern laufen statt erst beim Klick.", @@ -37,6 +39,7 @@ }, { "id": "card-hook", + "stable_id": "card-ms942h7v0cfij", "front": "Hook", "back": "Eine spezielle Funktion (Name beginnt mit „use“), die Funktionskomponenten Zusatzfähigkeiten wie State gibt.", "notes": "Beispiele: useState, useEffect. Hooks ruft man nur auf oberster Ebene der Komponente auf.", @@ -49,6 +52,7 @@ }, { "id": "card-rerender-ausloeser", + "stable_id": "card-ms942h7v0cge1", "front": "Was löst ein Re-Render aus?", "back": "Eine Änderung des eigenen States oder neue Props von der Elternkomponente.", "notes": "Rendert ein Elternteil neu, rendern in der Regel auch seine Kinder neu.", @@ -61,6 +65,7 @@ }, { "id": "card-komposition", + "stable_id": "card-ms942h7v0ch3h", "front": "Komponenten-Komposition", "back": "Größere Oberflächen entstehen, indem man kleinere Komponenten ineinander verschachtelt.", "notes": "Statt einer riesigen Komponente kombiniert man viele kleine, wiederverwendbare.", @@ -73,6 +78,7 @@ }, { "id": "card-eltern-kind-fluss", + "stable_id": "card-ms942h7v0cigd", "front": "Eltern-zu-Kind-Datenfluss", "back": "Daten reicht man über Props von Eltern an Kinder weiter; ein Kind meldet Ereignisse über Callback-Props zurück.", "notes": "Der State liegt oft beim Elternteil; das Kind bekommt Werte als Props und ruft übergebene Funktionen auf.", @@ -103,6 +109,7 @@ "title": "Klick behandeln", "exercise": { "id": "ex-mc-onclick", + "stable_id": "ex-ms942h7v0cj55", "type": "cloze", "prompt": "Wie lässt man in React eine Funktion auf einen Klick reagieren?", "card_ids": [ @@ -131,6 +138,7 @@ "title": "Handler richtig übergeben", "exercise": { "id": "ex-mc-handler-referenz", + "stable_id": "ex-ms942h7v0ckmk", "type": "cloze", "prompt": "Was ist der Unterschied zwischen onClick={handleKlick} und onClick={handleKlick()}?", "card_ids": [ @@ -159,6 +167,7 @@ "title": "Was ist ein Hook?", "exercise": { "id": "ex-mc-hook", + "stable_id": "ex-ms942h7v0clyy", "type": "cloze", "prompt": "Was ist ein Hook in React?", "card_ids": [ @@ -187,6 +196,7 @@ "title": "Auslöser für ein Re-Render", "exercise": { "id": "ex-mc-rerender-ausloeser", + "stable_id": "ex-ms942h7v0cmmb", "type": "cloze", "prompt": "Wodurch wird ein erneutes Rendern (Re-Render) einer Komponente typischerweise ausgelöst?", "card_ids": [ @@ -215,6 +225,7 @@ "title": "Komponenten-Komposition", "exercise": { "id": "ex-mc-komposition", + "stable_id": "ex-ms942h7v0cnsc", "type": "cloze", "prompt": "Was bedeutet Komponenten-Komposition?", "card_ids": [ @@ -244,6 +255,7 @@ "title": "Lückentext: Hook-Namen", "exercise": { "id": "ex-cloze-hook-praefix", + "stable_id": "ex-ms942h7v0comk", "type": "cloze", "prompt": "Ergänze das Präfix, mit dem die Namen aller Hooks beginnen.", "card_ids": [ @@ -267,6 +279,7 @@ "title": "Begriffe zuordnen", "exercise": { "id": "ex-match-begriffe", + "stable_id": "ex-ms942h7v0cpsk", "type": "matching", "prompt": "Ordne jedem Begriff die passende Kurzbeschreibung zu.", "card_ids": [