@lernapps/tooling 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/dist/check/rules/competencies-on-map.mjs +85 -0
  2. package/dist/check/rules/one-competency-per-page.mjs +18 -0
  3. package/dist/check/rules/prerequisites-linked.mjs +38 -0
  4. package/dist/cli.mjs +191 -52
  5. package/dist/explainer-map-DW4XCcY0.mjs +124 -0
  6. package/dist/skills/lernapps-app/SKILL.md +2 -1
  7. package/dist/skills/lernapps-cli/SKILL.md +13 -4
  8. package/dist/skills/lernapps-content-questions/SKILL.md +2 -0
  9. package/dist/skills/lernapps-content-questions/references/question-types.md +2 -1
  10. package/dist/skills/lernapps-content-research/SKILL.md +41 -5
  11. package/dist/skills/lernapps-exercise-patterns/references/mathematics.md +25 -1
  12. package/dist/skills/lernapps-exercise-patterns/references/physics.md +1 -0
  13. package/dist/skills/lernapps-explainer/SKILL.md +10 -4
  14. package/dist/skills/lernapps-explainer/references/blocks.md +7 -3
  15. package/dist/skills/lernapps-explainer/references/exercises.md +6 -0
  16. package/dist/skills/lernapps-explainer/references/rules.md +29 -0
  17. package/dist/skills/lernapps-explainer/references/structure.md +62 -0
  18. package/dist/skills/lernapps-explainer-design/SKILL.md +46 -2
  19. package/dist/skills/lernapps-explainer-design/references/rules.md +1 -0
  20. package/dist/skills/lernapps-explainer-self-assessment/SKILL.md +1 -0
  21. package/dist/skills/lernapps-listing/SKILL.md +9 -0
  22. package/generator/create.ts +39 -10
  23. package/generator/init.ts +89 -19
  24. package/generator/update.ts +53 -8
  25. package/guidance/increment-template.md +13 -5
  26. package/guidance/plan-template.md +25 -11
  27. package/guidance/workflow-rules.md +3 -1
  28. package/package.json +2 -2
  29. package/skills/archetypes/lernapps-explainer/SKILL.md +10 -4
  30. package/skills/archetypes/lernapps-explainer/references/blocks.md +7 -3
  31. package/skills/archetypes/lernapps-explainer/references/exercises.md +6 -0
  32. package/skills/archetypes/lernapps-explainer/references/rules.md +29 -0
  33. package/skills/archetypes/lernapps-explainer/references/structure.md +62 -0
  34. package/skills/archetypes/lernapps-explainer-design/SKILL.md +46 -2
  35. package/skills/archetypes/lernapps-explainer-design/references/rules.md +1 -0
  36. package/skills/archetypes/lernapps-explainer-self-assessment/SKILL.md +1 -0
  37. package/skills/content/lernapps-content-questions/SKILL.md +2 -0
  38. package/skills/content/lernapps-content-questions/references/question-types.md +2 -1
  39. package/skills/content/lernapps-content-research/SKILL.md +41 -5
  40. package/skills/content/lernapps-exercise-patterns/references/mathematics.md +25 -1
  41. package/skills/content/lernapps-exercise-patterns/references/physics.md +1 -0
  42. package/skills/process/lernapps-app/SKILL.md +2 -1
  43. package/skills/process/lernapps-listing/SKILL.md +9 -0
  44. package/skills/tooling/lernapps-cli/SKILL.md +13 -4
package/generator/init.ts CHANGED
@@ -6,25 +6,28 @@
6
6
  // plan has chosen one: `skills --archetype <name>` then adds that archetype's. `lernapps create` later takes over the
7
7
  // package.json (the marker lernapps.init) and the installed packages.
8
8
  import { spawnSync } from "node:child_process";
9
- import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
9
+ import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
10
10
  import { createRequire } from "node:module";
11
+ import { tmpdir } from "node:os";
11
12
  import { dirname, join, resolve } from "node:path";
12
13
  import { parseArgs, type ParseArgsOptionsConfig } from "node:util";
13
14
  import { UsageError } from "../src/usage.ts";
14
15
  import { PLANS } from "../src/plan.ts";
15
- import { CreateError, fromPackage, ownVersion, readJson, TOOLING, writeJson } from "./create.ts";
16
+ import { CreateError, fromPackage, ownVersion, readJson, TOOLING, WORK_BRANCH, writeJson } from "./create.ts";
16
17
 
17
18
  /** The options of `lernapps init`, as node:util parseArgs takes them. */
18
19
  export const INIT_OPTIONS = {
19
20
  addon: { type: "string" },
21
+ tooling: { type: "string" },
20
22
  help: { type: "boolean", short: "h", default: false },
21
23
  } as const satisfies ParseArgsOptionsConfig;
22
24
 
23
- export const INIT_USAGE = `Usage: lernapps init <short name> [--addon <package>]
25
+ export const INIT_USAGE = `Usage: lernapps init <short name> [--addon <package>] [--tooling <spec>]
24
26
 
25
27
  Starts an app in the current folder, which must be empty except for .git. It needs git, Node and the npm registry; nothing
26
28
  else. In order:
27
- git repository on the branch work, which the plan names (unless the folder is one already; no commit)
29
+ git repository on the branch work, which the plan names; no commit. A repository without commits
30
+ switches to work; one with commits stays, and init warns: git switch -c work
28
31
  package.json names the folder, depends on @lernapps/tooling (this version) and the add-on
29
32
  npm install installs them
30
33
  .vibe/plans/01-<short name>.md
@@ -36,6 +39,12 @@ Then follow the plan: it says what to do, and when to wait for the creator.
36
39
 
37
40
  Options:
38
41
  --addon <package> a package that extends the tooling, e.g. @lernapps/practice: its plan template and its skills
42
+ --tooling <spec> for maintainers: the npm spec of @lernapps/tooling to install instead of this version, e.g.
43
+ file:/path/to/lernapps-tooling-0.0.0.tgz or github:lernapps/tooling#<commit>; give the same
44
+ to lernapps create. Without it, a tooling of version 0.0.0 (a local build, not on the
45
+ registry) is packed with npm pack into a temporary folder and installed from that tarball
46
+ when its folder is a git checkout (file:<tarball>), with its dependencies, as from the
47
+ registry
39
48
  -h, --help print this help
40
49
  `;
41
50
 
@@ -62,22 +71,45 @@ const SKILLS_BIN = () => {
62
71
  return join(dirname(manifest), bin?.["skills"] ?? "bin/cli.mjs");
63
72
  };
64
73
 
65
- /** The branch the plan works on (rule 8 of the plan template). */
66
- export const WORK_BRANCH = "work";
74
+ export { WORK_BRANCH };
75
+
76
+ /** A git command in `dir`; a git that cannot run is an error of init. */
77
+ function git(dir: string, args: string[]) {
78
+ const result = spawnSync("git", args, { cwd: dir, encoding: "utf8" });
79
+ if (result.error) throw new CreateError(`git cannot run: ${result.error.message}. Install git, then run init again`);
80
+ return { ok: result.status === 0, out: `${result.stdout}${result.stderr}`.trim() };
81
+ }
82
+
83
+ /** What `ensureRepository` did: a line for the output, and a warning when the plan's branch is not checked out. */
84
+ export interface Repository {
85
+ done?: string;
86
+ warning?: string;
87
+ }
67
88
 
68
89
  /**
69
- * Makes the folder a git repository on the branch `work`, which the plan names, unless it is one already: `create` and
70
- * the hooks need it, and the plan's first rule is to work there. Returns what it did, for the output; no commit.
90
+ * Puts the folder on the branch `work`, which the plan names: `create` and the hooks need a repository, and the plan's
91
+ * first rule is to work there. No repository yet → `git init` on `work`. A repository without commits (a fresh
92
+ * `git init` on master or main, a clone of an empty GitHub repo) → its unborn branch becomes `work`: nothing is lost,
93
+ * since there is nothing on it. A repository with commits on another branch is left as it is, with a warning that
94
+ * names the command. No commit.
71
95
  */
72
- export function ensureRepository(dir: string): string | undefined {
73
- if (existsSync(join(dir, ".git"))) return undefined;
74
- const result = spawnSync("git", ["init", "--quiet", `--initial-branch=${WORK_BRANCH}`], {
75
- cwd: dir,
76
- encoding: "utf8",
77
- });
78
- if (result.error) throw new CreateError(`git cannot run: ${result.error.message}. Install git, then run init again`);
79
- if (result.status !== 0) throw new CreateError(`git init failed: ${`${result.stdout}${result.stderr}`.trim()}`);
80
- return `a git repository on the branch ${WORK_BRANCH}`;
96
+ export function ensureRepository(dir: string): Repository {
97
+ if (!existsSync(join(dir, ".git"))) {
98
+ const result = git(dir, ["init", "--quiet", `--initial-branch=${WORK_BRANCH}`]);
99
+ if (!result.ok) throw new CreateError(`git init failed: ${result.out}`);
100
+ return { done: `a git repository on the branch ${WORK_BRANCH}` };
101
+ }
102
+ const branch = git(dir, ["symbolic-ref", "--quiet", "--short", "HEAD"]);
103
+ if (branch.ok && branch.out === WORK_BRANCH) return {};
104
+ if (!git(dir, ["rev-parse", "--verify", "--quiet", "HEAD"]).ok) {
105
+ const result = git(dir, ["symbolic-ref", "HEAD", `refs/heads/${WORK_BRANCH}`]);
106
+ if (!result.ok) throw new CreateError(`git could not switch to the branch ${WORK_BRANCH}: ${result.out}`);
107
+ return { done: `the repository had no commit yet: now on the branch ${WORK_BRANCH}` };
108
+ }
109
+ const where = branch.ok ? `on the branch ${branch.out}` : "on no branch (detached HEAD)";
110
+ return {
111
+ warning: `the repository has commits and is ${where}, not on ${WORK_BRANCH}, which the plan works on. Run next: git switch -c ${WORK_BRANCH}`,
112
+ };
81
113
  }
82
114
 
83
115
  /** The names of the skills of the archetypes, with the archetype each belongs to (lernapps-<archetype>[-…]). */
@@ -121,6 +153,42 @@ function syncSkills(dir: string, exclude: string[]): void {
121
153
  );
122
154
  }
123
155
 
156
+ /** The version of a build that is not on the registry: package.json of the repository says 0.0.0. */
157
+ const LOCAL_VERSION = "0.0.0";
158
+
159
+ /**
160
+ * The tarball of the package in `root`, packed by npm pack (with its prepare script, so the build is current) into a
161
+ * new temporary folder.
162
+ */
163
+ export function packBuild(root: string): string {
164
+ const out = mkdtempSync(join(tmpdir(), "lernapps-tooling-"));
165
+ run("npm", ["pack", "--silent", "--pack-destination", out], (message) => new CreateError(message), root);
166
+ const tarball = readdirSync(out).find((name) => name.endsWith(".tgz"));
167
+ if (tarball === undefined) throw new CreateError(`npm pack in ${root} wrote no tarball into ${out}`);
168
+ return join(out, tarball);
169
+ }
170
+
171
+ /**
172
+ * The spec of @lernapps/tooling that init installs: the one given, else this version. A local build (version 0.0.0,
173
+ * which the registry does not have) is packed when its folder is a git checkout and installed from the tarball, else
174
+ * init says to name one with --tooling. Not the folder itself (file:<folder>): npm links it, so the app's
175
+ * node_modules lacks the tooling's dependencies (Playwright for the end-to-end tests, @lernapps/app-templates with
176
+ * the templates and their READMEs); from a tarball they are installed into the app, as from the registry.
177
+ */
178
+ export function toolingSpec(
179
+ given: string | undefined,
180
+ version = ownVersion(),
181
+ root = fromPackage(),
182
+ pack: (root: string) => string = packBuild,
183
+ ): string {
184
+ if (given !== undefined && given !== "") return given;
185
+ if (version !== LOCAL_VERSION) return version;
186
+ if (existsSync(join(root, ".git"))) return `file:${pack(resolve(root))}`;
187
+ throw new CreateError(
188
+ `this @lernapps/tooling is a local build (version ${LOCAL_VERSION}), which the npm registry does not have: name the tooling to install with --tooling <spec>, e.g. file:/path/to/lernapps-tooling-${LOCAL_VERSION}.tgz (npm pack) or github:lernapps/tooling#<commit>, and give the same to lernapps create`,
189
+ );
190
+ }
191
+
124
192
  export function init(args: string[]): number {
125
193
  const { values, positionals } = parseArgs({ args, options: INIT_OPTIONS, strict: true, allowPositionals: true });
126
194
  if (values.help) {
@@ -139,6 +207,7 @@ export function init(args: string[]): number {
139
207
  );
140
208
  }
141
209
  const addon = values.addon;
210
+ const tooling = toolingSpec(values.tooling);
142
211
  const repository = ensureRepository(dir);
143
212
  writeJson(join(dir, "package.json"), {
144
213
  name,
@@ -149,7 +218,7 @@ export function init(args: string[]): number {
149
218
  const fail = (message: string) => new CreateError(message);
150
219
  run(
151
220
  "npm",
152
- ["install", "--save-dev", "--no-audit", "--no-fund", `${TOOLING}@${ownVersion()}`, ...(addon ? [addon] : [])],
221
+ ["install", "--save-dev", "--no-audit", "--no-fund", `${TOOLING}@${tooling}`, ...(addon ? [addon] : [])],
153
222
  fail,
154
223
  );
155
224
 
@@ -167,8 +236,9 @@ export function init(args: string[]): number {
167
236
  dir,
168
237
  archetypeSkills().map((skill) => skill.name),
169
238
  );
239
+ if (repository.warning !== undefined) process.stderr.write(`lernapps init: warning: ${repository.warning}\n`);
170
240
  process.stdout.write(
171
- `lernapps init: ${relative(dir, target)} from ${relativeTemplate(template, dir)}; the skills of the packages are installed${repository ? `; ${repository}` : ""}\nNext: read ${relative(dir, target)} completely and follow it\n`,
241
+ `lernapps init: ${relative(dir, target)} from ${relativeTemplate(template, dir)}; the skills of the packages are installed${repository.done ? `; ${repository.done}` : ""}\nNext: ${repository.warning ? `git switch -c ${WORK_BRANCH}, then ` : ""}read ${relative(dir, target)} completely and follow it\n`,
172
242
  );
173
243
  return 0;
174
244
  }
@@ -7,7 +7,8 @@
7
7
  // them replaced with versions.
8
8
  // It never moves the runtime back: a newer version (Renovate moves the runtime too) stays. An app made before the
9
9
  // generator wrote the content guide gets its skeleton (generator/create.ts), and one made before the plans had a
10
- // folder gets its plan .vibe/plan.md moved to .vibe/plans/01-first-version.md (src/plan.ts). The app's lernapps-*
10
+ // folder gets its plan .vibe/plan.md moved to .vibe/plans/01-first-version.md (src/plan.ts). An explainer in format 1 gets `format: 2` and `strand: none` in src/app.yaml where the runtime knows
11
+ // format 2. The app's lernapps-*
11
12
  // skills, which the app commits, are replaced with those of this tooling (its skills/), in the layout of
12
13
  // `npx skills add`; the app's own skills (app-content, any other) stay as they are. An interactive app's architecture
13
14
  // is replaced outside its app sections, which stay as they are (generator/architecture.ts).
@@ -40,6 +41,7 @@ import {
40
41
  ownVersion,
41
42
  readJson,
42
43
  RUNTIME,
44
+ runtimeSpec,
43
45
  SKILLS_LOCK,
44
46
  templatesPackage,
45
47
  TOOLING,
@@ -57,13 +59,16 @@ export const UPDATE_USAGE = `Usage: lernapps update
57
59
  Moves the app in the current folder to the versions of this @lernapps/tooling, then runs npm install. npm update
58
60
  moves the app within its version ranges; this moves the ranges, and what npm does not know:
59
61
  @lernapps/app-templates to the version this tooling depends on: the runtime its tests ran with. A newer version
60
- (e.g. from Renovate) is kept. A git commit, as apps once pinned, becomes a version
62
+ (e.g. from Renovate) is kept. A git commit, as apps once pinned, becomes a version; while
63
+ this tooling depends on a commit (an unreleased runtime), the app moves to that commit
61
64
  @lernapps/tooling to the version of this tooling (unchanged when run from the app)
62
65
  .github/workflows/ the actions of lernapps/tooling at the tag of that version
63
66
  .agents/skills/app-content/SKILL.md
64
67
  the content guide's skeleton, where the app has none yet (and its link for Claude Code)
65
68
  .vibe/plan.md the plan of an app made before the plans had a folder: moved to
66
69
  .vibe/plans/01-first-version.md, the first of its plans (one per piece of work)
70
+ src/app.yaml of an explainer in format 1, when the runtime knows format 2: adds format: 2 and
71
+ strand: none (the app is not on the learning map; its pages need no competencies)
67
72
  .agents/skills/lernapps-*, .claude/skills/lernapps-*, skills-lock.json
68
73
  replaced with the skills of this tooling, as npx skills add lays them out: those of
69
74
  every phase, and of the archetypes only those of the current plan's archetype; every
@@ -98,9 +103,16 @@ function lowest(range: string): [number, number, number] | undefined {
98
103
  const older = (a: [number, number, number], b: [number, number, number]) =>
99
104
  a[0] !== b[0] ? a[0] < b[0] : a[1] !== b[1] ? a[1] < b[1] : a[2] < b[2];
100
105
 
101
- /** The next range of a dependency at `version`, and what the output says about it. */
102
- export function nextRange(name: string, current: string | undefined, version: string): { spec: string; line: string } {
103
- const target = `^${version}`;
106
+ /**
107
+ * The next range of a dependency at `version` (target: ^ that version, or the spec given, e.g. a git commit of an
108
+ * unreleased runtime), and what the output says about it.
109
+ */
110
+ export function nextRange(
111
+ name: string,
112
+ current: string | undefined,
113
+ version: string,
114
+ target = `^${version}`,
115
+ ): { spec: string; line: string } {
104
116
  // a local copy (a folder or a tarball of the package, as an app under development uses) is the creator's choice
105
117
  if (current !== undefined && /^(file:|link:|\.{0,2}\/)/.test(current)) {
106
118
  return { spec: current, line: `${name}: kept ${current}, a local copy` };
@@ -108,8 +120,9 @@ export function nextRange(name: string, current: string | undefined, version: st
108
120
  const now = current === undefined ? undefined : lowest(current);
109
121
  const next = lowest(version);
110
122
  if (current === target) return { spec: current, line: `${name}: unchanged (${current})` };
111
- // a newer version than this tooling's stays: it must never move back
112
- if (now !== undefined && next !== undefined && !older(now, next)) {
123
+ // a newer version than this tooling's stays: it must never move back; a commit (no version to compare) is where the
124
+ // tooling's tests ran, so the app moves to it
125
+ if (lowest(target) !== undefined && now !== undefined && next !== undefined && !older(now, next)) {
113
126
  return { spec: current ?? target, line: `${name}: kept ${current}, not moved to ${target}: it is not older` };
114
127
  }
115
128
  return { spec: target, line: `${name}: ${current ?? "none"} → ${target}` };
@@ -148,6 +161,35 @@ function migratePlan(): string[] {
148
161
  ];
149
162
  }
150
163
 
164
+ /** The explainer's app file, and the schema file that tells the runtime knows format 2 (learning map). */
165
+ const EXPLAINER_APP = join("src", "app.yaml");
166
+ const EXPLAINER_V2_SCHEMA = join("runtime", "explainer", "explainer.v2.schema.json");
167
+
168
+ /**
169
+ * Moves an explainer from format 1 to format 2 (the learning map): adds `format: 2` and `strand: none` to
170
+ * src/app.yaml when the app has neither, and points its schema comment to explainer.v2.schema.json. `strand: none`
171
+ * keeps the app off the map, so its pages need no competencies; the creator may name a strand later (skill
172
+ * lernapps-content-research). Only where the runtime of this tooling has the v2 schema: an older runtime does not
173
+ * know the fields. An app that is no explainer, or already has a `format` or a `strand`, stays as it is. What it did,
174
+ * as lines of the output.
175
+ */
176
+ export function migrateExplainer(templatesRoot: string): string[] {
177
+ if (!existsSync(EXPLAINER_APP) || !existsSync(join(templatesRoot, EXPLAINER_V2_SCHEMA))) return [];
178
+ const text = readFileSync(EXPLAINER_APP, "utf8");
179
+ const top = (key: string) => new RegExp(`^${key}:`, "m").test(text);
180
+ if (!top("chapters") || top("format") || top("strand")) return [];
181
+ const schema = /^(# yaml-language-server: \$schema=.*)explainer\.v1\.schema\.json$/m;
182
+ const fields = "format: 2\nstrand: none\n";
183
+ let next: string;
184
+ if (schema.test(text))
185
+ next = text.replace(schema, (_all, head: string) => `${head}explainer.v2.schema.json\n${fields.trimEnd()}`);
186
+ else next = fields + text;
187
+ writeFileSync(EXPLAINER_APP, next);
188
+ return [
189
+ `explainer: ${EXPLAINER_APP} moved to format 2: added format: 2 and strand: none (not on the learning map; pages need no competencies). To join the map, pick a strand of https://lernapps.net/apps/map/ and give every page competencies`,
190
+ ];
191
+ }
192
+
151
193
  /**
152
194
  * The skills of this tooling, as it ships them (skills/<group>/<name>/), and as the app keeps them (flat, by name).
153
195
  * Every app has those of the groups process, tooling and content; of the group archetypes only those of its own
@@ -371,7 +413,7 @@ export function update(args: string[]): number {
371
413
  const notMoved: string[] = [];
372
414
  const runtime = devDependencies[RUNTIME];
373
415
  if (runtime !== undefined) {
374
- const next = nextRange(RUNTIME, runtime, templates.version);
416
+ const next = nextRange(RUNTIME, runtime, templates.version, runtimeSpec(templates.version));
375
417
  if (next.spec !== runtime) specs.push(`${RUNTIME}@${next.spec}`);
376
418
  lines.push(next.line);
377
419
  }
@@ -404,6 +446,8 @@ export function update(args: string[]): number {
404
446
  lines.push(...guide.map((line) => `content guide: ${line}`));
405
447
  const plan = migratePlan();
406
448
  lines.push(...plan);
449
+ const explainer = migrateExplainer(templates.root);
450
+ lines.push(...explainer);
407
451
  const skills = [...replaceSkills(), ...unignoreSkills()];
408
452
  lines.push(...skills);
409
453
  const architecture = updateArchitecture();
@@ -421,6 +465,7 @@ export function update(args: string[]): number {
421
465
  if (skillsMoved) files.push(SKILLS_LOCK);
422
466
  if (skills.some((line) => line.includes(".gitignore"))) files.push(".gitignore");
423
467
  if (architecture.lines.length > 0) files.push(`${ARCHITECTURE}/`);
468
+ if (explainer.length > 0) files.push(EXPLAINER_APP);
424
469
  if (plan.some((line) => line.startsWith("plan: moved"))) files.push(".vibe/ (git add -A .vibe, to keep the move)");
425
470
  process.stdout.write(
426
471
  `Next: npx --no -- lernapps check, then commit ${files.slice(0, -1).join(", ")} and ${files.at(-1) ?? ""}\n`,
@@ -45,7 +45,7 @@ a correction.
45
45
 
46
46
  ## Notes
47
47
 
48
- <!-- Curriculum orientation, new competencies by chapter, didactic approach, scope, sources and licences, open
48
+ <!-- Curriculum orientation, new competencies by chapter (ids of the strand), didactic approach, scope, sources and licences, open
49
49
  questions. -->
50
50
 
51
51
  ## Explore
@@ -68,12 +68,19 @@ questions. -->
68
68
  "(guess)". Topics: which competencies, chapters or rounds, for which school years; hurdles and examples;
69
69
  amount; any change of look and feel or terms.
70
70
  - [ ] Load skill `lernapps-content-research`. Research what the guide's curriculum and sources do not cover. Under
71
- "Notes": curriculum orientation, new competencies by chapter as "Ich kann …", verified sources.
71
+ "Notes": curriculum orientation, new competencies by chapter as "Ich kann …", verified sources. The app keeps its
72
+ strand; with one, the new competencies are only those of that strand in the learning map (ids), as skill
73
+ `lernapps-content-research` says, an explainer as a rule one page each; competencies that build on each other (the
74
+ map's `requires`) share a chapter, in that order. None in the map → propose it to the
75
+ catalog. `strand: none` and a strand fits → recommend it.
72
76
  - [ ] Under "Notes": the didactic approach per competency and the scope (in, out, data range).
73
77
  - [ ] A kind of content the guide does not cover (a new page structure, screen, component or kind of exercise) →
74
78
  load skills `lernapps-<archetype>-design` and `lernapps-exercise-patterns`. Else skip this task.
75
- - [ ] **Checkpoint:** research. Show in plain words: curriculum orientation, new competencies by chapter, didactic
76
- approach, scope, every "(guess)", sources and how you checked them, what is unchecked. Stop and wait.
79
+ - [ ] **Checkpoint:** research. Show in plain words: curriculum orientation, new competencies by chapter (with their
80
+ ids), didactic
81
+ approach, scope, every "(guess)", sources and how you checked them, what is unchecked. With a strand, the map
82
+ decides the structure: new competencies that build on each other (its `requires`) share a chapter, in that
83
+ order; no curriculum search is needed for it. Stop and wait.
77
84
  Changes → change, ask again.
78
85
  - [ ] Set `phase: plan`: change the value line (`grep '^phase:'` prints `phase: plan`).
79
86
 
@@ -110,7 +117,8 @@ questions. -->
110
117
  ### Content method
111
118
 
112
119
  <!-- One line each: the guide's patterns, figures and components used, and where; anything new, what it does and
113
- cannot. Something new goes into the guide in the slice that adds it. -->
120
+ cannot; an explainer on the map: per new page its prerequisites the app does not practise, and which get a recap
121
+ page, which a link. Something new goes into the guide in the slice that adds it. -->
114
122
 
115
123
  ### Completed
116
124
 
@@ -42,7 +42,7 @@ For you, the assistant.
42
42
 
43
43
  ## Notes
44
44
 
45
- <!-- Curriculum orientation, competencies by chapter, didactic approach, scope, sources and licences, open
45
+ <!-- Strand (<subject>/<strand> or none), curriculum orientation, competencies by chapter (ids), didactic approach, scope, sources and licences, open
46
46
  questions. -->
47
47
 
48
48
  ## Explore
@@ -59,7 +59,7 @@ questions. -->
59
59
 
60
60
  - [ ] The skills of the phases are in `.agents/skills/` (`npx --yes @lernapps/tooling init <short name>` put them there
61
61
  and wrote this plan; the app commits them). `.agents/skills/lernapps-app` missing → stop and tell the creator
62
- the folder was not started with `init`.
62
+ the folder was not started with `init`. `git branch --show-current` prints `work`; else `git switch -c work`.
63
63
  - [ ] Load skill `lernapps-app`. Tell the creator in two sentences what a lernapps app is and where it will live: at
64
64
  their own address, free, public, and once listed as a copy on lernapps.net. The technical part (GitHub) comes at the end, and you do it.
65
65
  - [ ] Interview: ask the topics "Goal" and "Learners" of "Answers".
@@ -74,7 +74,11 @@ questions. -->
74
74
  which. No answer → ask once more; still none → write your assumption with "(guess)". Done when every topic
75
75
  has an answer or a guess.
76
76
  - [ ] Load skill `lernapps-content-research`. Research as it says. Under "Notes": curriculum orientation,
77
- competencies by chapter as "Ich kann …", verified sources. No competencies → not done.
77
+ competencies by chapter as "Ich kann …", verified sources. No competencies → not done. First the learning map as
78
+ skill `lernapps-content-research` says: recommend a strand that fits (`<subject>/<strand>`), else `none`; with a
79
+ strand, only its competencies, by their ids from the map. Write both under "Notes", and how they map onto
80
+ chapters and pages (an explainer: as a rule one page per competency; competencies that build on each other, by
81
+ the map's `requires`, share a chapter, in that order).
78
82
  - [ ] Under "Notes", one line per competency: how learners get there (derive, not only read off; e.g. a tree diagram
79
83
  shows how a probability comes about), which misconception the feedback catches.
80
84
  - [ ] Under "Notes", the scope: what is in, what is out and why, the data range (e.g. "all main-group elements",
@@ -82,8 +86,10 @@ questions. -->
82
86
  - [ ] Fetch https://lernapps.net/apps/schemas/entry.v1.schema.json and read each property's description. Ask what
83
87
  helps teachers and parents find the app. Fill "Entry draft". Search words: one or two words each, as people
84
88
  type them.
85
- - [ ] **Checkpoint:** research. Show in plain words: curriculum orientation, competencies by chapter, didactic
86
- approach, scope, every "(guess)", sources and how you checked them, what is unchecked, archetype and reason.
89
+ - [ ] **Checkpoint:** research. Show in plain words: the strand or why none, curriculum orientation, competencies by
90
+ chapter (with their ids) and their pages, didactic approach, scope, every "(guess)", sources and how you
91
+ checked them, what is unchecked, archetype and reason. With a strand, the map decides the structure: competencies
92
+ that build on each other (its `requires`) share a chapter, in that order; no curriculum search is needed for it.
87
93
  Stop and wait. Changes → change, ask again.
88
94
  - [ ] Set `phase: plan`: change the value line (`grep '^phase:'` prints `phase: plan`).
89
95
 
@@ -132,11 +138,12 @@ questions. -->
132
138
  - [ ] Map the content as skill `lernapps-<archetype>-design` says; reread it if it is no longer in view.
133
139
  - [ ] Choose a pattern per competency as skill `lernapps-exercise-patterns` says (loaded in Explore; reread it if it
134
140
  is no longer in view). Write "Content method".
135
- - [ ] Write the Code tasks under "Code": pages or screens, their content, what each shows. Each ends in a commit.
141
+ - [ ] Write the Code tasks under "Code": pages or screens, their content, what each shows. Each ends in a commit:
142
+ one task, one commit. Steps too small for a commit of their own → one task.
136
143
  After the first task that shows real content: the task "Show the creator a first look (`early-preview`)".
137
144
  - [ ] Each screen, component or interactive exercise: a sketch (static SVG or image, or the steps with what
138
145
  learners see after each). Words alone are not enough. Keep sketch files in `.vibe/sketches/`, which `create` leaves alone.
139
- - [ ] Show the creator, short: goal, competencies by chapter, entry draft, tasks, sketches; the content method in
146
+ - [ ] Show the creator, short: goal, competencies by chapter (chapters along the map's `requires`), entry draft, tasks, sketches; the content method in
140
147
  plain words (what learners see and do, what it cannot do). At most 2 questions.
141
148
  - [ ] **Checkpoint:** confirm the plan and the content method, with the sketches shown. Stop and wait for an
142
149
  explicit yes. Changes → change, ask again. No `create` before the yes.
@@ -145,7 +152,8 @@ questions. -->
145
152
  ### Content method
146
153
 
147
154
  <!-- One line each: exercise patterns and where; figures and how they are made; data files; each component, what it
148
- checks and cannot; how a later agent adds a chapter. Becomes the content guide in Code. -->
155
+ checks and cannot; how a later agent adds a chapter. An explainer on the map: per page its prerequisites (`requires`)
156
+ the app does not practise, and which get a recap page, which a link. Becomes the content guide in Code. -->
149
157
 
150
158
  ### Completed
151
159
 
@@ -153,7 +161,8 @@ checks and cannot; how a later agent adds a chapter. Becomes the content guide i
153
161
 
154
162
  <!--
155
163
  - Build the app from the archetype and the confirmed plan, locally on `work`. Work on your own.
156
- - Commit after each task; the hooks check. A check fails → skill `lernapps-cli`. Fix from the message; never
164
+ - Commit after each task, literally: one task, one commit, its box ticked in it; the hooks check. Never one commit
165
+ for several tasks: join them into one task in the plan first. A check fails → skill `lernapps-cli`. Fix from the message; never
157
166
  bypass a hook.
158
167
  - Undecidable → choose, mark "(guess)" under "Key decisions", go on.
159
168
  - Creator asks for a change → first say your reading in one sentence ("Ich nehme alle 44 Hauptgruppenelemente
@@ -168,10 +177,15 @@ checks and cannot; how a later agent adds a chapter. Becomes the content guide i
168
177
  - [ ] Load skill `lernapps-cli`. Create the app in this plan's folder as it says. `create` keeps this plan. Never
169
178
  write the app by hand.
170
179
  - [ ] Fill the content guide `.agents/skills/app-content/SKILL.md` from "Notes" and "Content method"
171
- (`app-content-guide`). `grep -c "FILL IN" .agents/skills/app-content/SKILL.md` prints 0. Commit it before
172
- any content.
180
+ (`app-content-guide`). Keep its nine headings and fill each: Curriculum, Competencies, Content structure,
181
+ Terminology, Exercise patterns, Figures, Sources, Adding a chapter or round, Rules learned from mistakes
182
+ ("None yet." until the first). `grep -c "FILL IN" .agents/skills/app-content/SKILL.md` prints 0. Commit it
183
+ before any content.
173
184
  - [ ] Load skill `lernapps-<archetype>` and the content guide. Questions with options and feedback: also skill
174
185
  `lernapps-content-questions`.
186
+ - [ ] An explainer: decide on `scope` as `references/structure.md` of skill `lernapps-explainer` says. Needed with an
187
+ `open-task` or the creator's material: then `scope` in `src/app.yaml` and on every page
188
+ (`explainer-scope-matches`). Write the choice under "Key decisions".
175
189
  - [ ] <the tasks written in Plan; after each, commit on `work`; after the first with real content, show the
176
190
  creator a first look (`early-preview`) and go on>
177
191
  - [ ] The content guide matches what was built (`app-content-matches`).
@@ -87,7 +87,9 @@ scope: tooling
87
87
  severity: error
88
88
  ```
89
89
 
90
- - Keep `.agents/skills/app-content/SKILL.md` with its fixed headings.
90
+ - Keep `.agents/skills/app-content/SKILL.md` with its fixed headings, in this order, each filled: Curriculum,
91
+ Competencies, Content structure, Terminology, Exercise patterns, Figures, Sources, Adding a chapter or round,
92
+ Rules learned from mistakes ("None yet." until the first rule).
91
93
  - After the plan is confirmed and before content: replace every placeholder ("FILL IN") with concrete instructions
92
94
  for writing new content: files, ids, data files, an example of this app. Not how the app works, no general advice.
93
95
  - A content decision changes (pattern, figure type, source rule) → change the guide in the same commit.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lernapps/tooling",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Shared tooling of the lernapps.net apps and sites: the CLI lernapps, site actions, Renovate preset, guidance for building apps; documentation at https://lernapps.net/tooling/",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -107,7 +107,7 @@
107
107
  "arc42": "arc42 --dir docs/arc42"
108
108
  },
109
109
  "dependencies": {
110
- "@lernapps/app-templates": "0.2.1",
110
+ "@lernapps/app-templates": "github:lernapps/app-templates#4fdc214e60ce4523d8233d3ed9b37641601b2fa9",
111
111
  "axe-core": "4.14.0",
112
112
  "playwright": "1.64.1",
113
113
  "skills": "1.7.4",
@@ -9,9 +9,10 @@ license: MIT
9
9
  - You write YAML in `src/`; a Vue component only where the plan names one. Rendering, navigation, formulas, figures,
10
10
  exercises, accessibility, languages and the no-JS page come from the runtime `@lernapps/app-templates`.
11
11
  - `create` writes a placeholder: one chapter with one page and one exercise. Replace it with the planned content. The complete example, the chapter "Prozent" with a component, is in `node_modules/@lernapps/app-templates/explainer/`: look things up there, never copy it.
12
- - Schema: `node_modules/@lernapps/app-templates/runtime/explainer/explainer.v1.schema.json`. Start every YAML file with
13
- `# yaml-language-server: $schema=../../node_modules/@lernapps/app-templates/runtime/explainer/explainer.v1.schema.json`
14
- (one `../` less in `src/app.yaml`).
12
+ - Schema: `node_modules/@lernapps/app-templates/runtime/explainer/explainer.v2.schema.json` (format 2: `format: 2`
13
+ in `src/app.yaml`, as `create` writes it). Start every YAML file with
14
+ `# yaml-language-server: $schema=../../node_modules/@lernapps/app-templates/runtime/explainer/explainer.v2.schema.json`
15
+ (one `../` less in `src/app.yaml`). Only an app that has not moved to format 2 keeps `explainer.v1.schema.json`.
15
16
  - Questions of the quiz's types: also skill `lernapps-content-questions`.
16
17
 
17
18
  ## Read when
@@ -22,7 +23,8 @@ license: MIT
22
23
  - [references/components.md](references/components.md): only when the plan names a component.
23
24
  - [references/runtime.md](references/runtime.md): when the build or a runtime test fails, and before you set up the
24
25
  tutor.
25
- - [references/rules.md](references/rules.md): when a check names an `explainer-*` or `vue-component-*` rule.
26
+ - [references/rules.md](references/rules.md): when a check names an `explainer-*`, `vue-component-*` or
27
+ `competencies-on-map` rule, and before you write a page with `competencies`.
26
28
 
27
29
  ## Gotchas
28
30
 
@@ -30,6 +32,10 @@ license: MIT
30
32
  - Headings are `section` blocks, never Markdown headings.
31
33
  - YAML: quote a text that has `: ` in it or looks like a number (`"8."`, `"01000001"`, `"0"`). Unquoted, YAML reads a map or a
32
34
  number: the build stops, or the digits change. A table cell, a header cell too, is a text and never empty.
35
+ - A page with `competencies` teaches what their "Ich kann …" in the map says, not clearly more or other
36
+ (`content-matches-competencies`).
37
+ - A page with formulas written through the shell: a heredoc with an unquoted delimiter replaces `$…` and `\`
38
+ (`<<EOF`). Quote the delimiter (`<<'EOF'`), or write the file with the file tool.
33
39
  - A new chapter: a folder with `chapter.yaml`, listed in `src/app.yaml`, each page listed in `chapter.yaml`.
34
40
  - `selfAssessment` in a `chapter.yaml`: written in Commit, skill
35
41
  `lernapps-explainer-self-assessment`. An exercise it names renamed or moved → change the reference too.
@@ -10,7 +10,7 @@
10
10
  | `section` | `id`, `title`, `blocks` | heading and blocks |
11
11
  | `text` | `text` | prose |
12
12
  | `formula` | `formula` (LaTeX subset, no `$`) | formula on its own line, MathML |
13
- | `figure` | `draw` (`hundred-grid` with `filled`, `bar` with `part`, `whole`) or `image` (`images/<name>.svg`, `source`, `licence`); `alt`, `caption` | inline SVG or bundled image |
13
+ | `figure` | `draw` (`hundred-grid` with `filled`, `bar` with `part`, `whole`) or `image` (`images/<name>.svg`, `source`, `licence`); `alt`, `caption` | inline SVG or bundled image; `bar` is one strip, the whole, with the part shaded and labelled with its percentage: a part of a whole only, never two quantities side by side or a value above the whole (an `image` or a `table` instead) |
14
14
  | `labelled-figure` | `image`, `alt`, `caption`, `labels` (`x`, `y` in %, `text`, optional `marker` of 1 to 3 characters, e.g. the material's `A` to `E`, else the number), `legend` (default `true`; `false` shows only the markers, then `alt` names every marker), `source`, `licence` | image with markers (letters or numbers) and a legend of the texts |
15
15
  | `table` | `caption`, `header`, `rowHeaders`, `rows` | table with caption |
16
16
  | `timeline` | `events` (`date`, `title`, `label`, `text`) or `data` | dated events, earliest first |
@@ -62,6 +62,10 @@
62
62
  - LaTeX subset → MathML; `formula` block or inline `$…$`.
63
63
  - Commands: `\frac`, `\cdot`, `\times`, `\div`, `\%`, `\text`, `^`, `_`, `\sqrt`, `\approx`, `\pm`, `\le`, `\ge`,
64
64
  `\neq`, `\,`, parentheses, braces (`\{`, `\}`).
65
+ - Trigonometry and more: `\sin`, `\cos`, `\tan`, `\arcsin` (or `\sin^{-1}`), `\arccos`, `\arctan`, Greek letters
66
+ (`\alpha`, `\varphi`, `\pi`, `\Delta`, …), degrees (`30^\circ`), spaces (`\ `, `\quad`, `\qquad`), `\Rightarrow`,
67
+ `\Leftrightarrow`, `\left(` … `\right)`:
68
+ `'\sin \alpha = \frac{a}{c} \quad \Rightarrow \quad a = c \cdot \sin \alpha'`.
65
69
  - Chemistry (mhchem): `\ce{2H2 + O2 -> 2H2O}`, `\ce{CO2 + H2O <=> H2CO3}`, units `\pu{123 kJ/mol}`.
66
70
  - Other command or parse error → build fails with the place.
67
71
  - YAML formulas in single quotes. Same in every language unless `\text{…}` differs (then a map).
@@ -80,6 +84,6 @@
80
84
 
81
85
  ## Data files
82
86
 
83
- - `src/data/<name>.yaml`: list of records, same lowercase fields; values number, boolean, text (map if several
84
- languages).
87
+ - `src/data/<name>.yaml`: list of records, same fields; a field name starts with a lower-case letter, then letters
88
+ and digits (`masse`, `hoeheM`; not `Masse`, `h_m`); values number, boolean, text (map if several languages).
85
89
  - Used by `timeline` (`data: ereignisse`) and `generated` (`from: { data: elemente }`). Each fact once, there.
@@ -27,6 +27,9 @@
27
27
  - New task per seed.
28
28
  - `given`: `int: [min, max]` + `step`; `decimal: [min, max]` + `step`; `oneOf: [...]`; `pick: <n>` `from:` list or
29
29
  `{ data: <name> }` (`distinct: true`; `pick: 1` = one record).
30
+ - An inline `from:` list holds numbers or texts only: a record in it (`{ de: …, en: … }`) is read as a text by
31
+ language and takes text values only. Records with numbers (`{ name: Stein, masse: 2 }`) → a data file
32
+ (`src/data/<name>.yaml`, `blocks.md`) and `from: { data: <name> }`.
30
33
  - `where`: boolean expressions; failing draws are redrawn.
31
34
  - `task`: `text`. A figure that changes with the task: an exercise component (`components.md`).
32
35
  - `answer`: `value` (expression) and `compare`: `number` or
@@ -36,6 +39,9 @@
36
39
  - Expressions (no eval): exact fractions, `+ - * /`, parentheses, `== != < <= > >=`, `and`, `or`, `not`, strings,
37
40
  fields (`e.year`), index (`xs[0]`), lists, `round(x, n)`, `decimals`, `map(xs, field)`,
38
41
  `sortBy(xs, field)`, `reverse`.
42
+ - `sqrt(x)`, `pow(x, n)`, `sin(x)`, `cos(x)`, `tan(x)`, `asin(x)`, `acos(x)`, `atan(x)`, angles in degrees
43
+ (`sin(30)` is `1/2`, `asin(0.5)` is `30`). A rational result is exact; any other (`sqrt(2)`, `sin(10)`) has 12
44
+ decimal places: give the answer with `round(…, n)` and `compare: { number: { decimals: n, tolerance } }`.
39
45
 
40
46
  ### Placeholders
41
47
 
@@ -283,3 +283,32 @@ severity: warning
283
283
  belong to it.
284
284
  - Each page's scope says what the page teaches, no more and no less; "Gehört nicht dazu" names what a learner or tutor
285
285
  might expect but the material does not teach.
286
+
287
+ ## `content-matches-competencies`: Each page teaches its competencies
288
+
289
+ ```rule
290
+ id: content-matches-competencies
291
+ scope: tooling
292
+ severity: hint
293
+ ```
294
+
295
+ - A page of a format-2 explainer with a strand teaches what the "Ich kann …" (`iCan`) of its competencies says in
296
+ `https://lernapps.net/apps/map/<subject>.json`: not clearly more, not something else.
297
+ - More to teach → a page of the competency that says it; none in the map → propose it to `lernapps/apps`
298
+ (`map/<subject>.yaml`), never invent it in the app.
299
+ - The review reads each page with the `iCan` of its competencies.
300
+
301
+ ## `competencies-on-map`: The strand and the page competencies are on the map
302
+
303
+ ```rule
304
+ id: competencies-on-map
305
+ scope: tooling
306
+ severity: warning
307
+ ```
308
+
309
+ - `strand` in `src/app.yaml` is a strand of the subject's map; each page's `competencies` are ids of that strand.
310
+ - A recap page's `recap` (`<subject>/<competency>`) is on the map, of any strand, and the app does not practise it
311
+ on a page of its own.
312
+ - `lernapps check` loads `https://lernapps.net/apps/map/<subject>.json` and warns before the push. Not reachable or
313
+ no map yet → one hint that it could not check.
314
+ - At the listing an unknown id or one of another strand is an error of the catalog (`entry-learning-map`).