@lernapps/tooling 0.3.1 → 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 +519 -56
  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 +3 -3
  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
@@ -8,8 +8,9 @@
8
8
  // Renovate configuration (generator/app/); an interactive app also its architecture (generator/architecture.ts). The
9
9
  // skills `lernapps init` synced into the folder stay, and are committed with the app; `lernapps update` replaces them
10
10
  // with those of the tooling it moves the app to.
11
- // To move to a new app-templates: set the exact version of @lernapps/app-templates in package.json; the generator's
12
- // tests then run on that version.
11
+ // To move to a new app-templates: set the exact version of @lernapps/app-templates in package.json, or a commit
12
+ // (github:lernapps/app-templates#<commit>, which Renovate moves) until a release has what the tooling needs; the
13
+ // generator's tests then run on that version. An app gets the same: ^ the version, or the same commit (runtimeSpec).
13
14
  import { spawnSync } from "node:child_process";
14
15
  import {
15
16
  cpSync,
@@ -49,7 +50,8 @@ node_modules init wrote). From the installed package @lernapps/app-templates, th
49
50
  on, it writes the archetype's scaffold: the files of the template folder <name>/ that the package's lernapps.scaffold
50
51
  lists (the entry, the configuration, the end-to-end tests of the content) and a placeholder as start content
51
52
  (scaffold/<name>/), which the planned content replaces. The template folder itself is the example app: it stays in
52
- node_modules/@lernapps/app-templates/<name>/, to look up. The app depends on the runtime of the same version.
53
+ node_modules/@lernapps/app-templates/<name>/, to look up. The app depends on the runtime of the same version (or
54
+ the same commit, while @lernapps/tooling depends on an unreleased one).
53
55
 
54
56
  It needs the plan of the first version, confirmed by the creator: started from the plan template as
55
57
  .vibe/plans/01-<short name>.md, the only plan there, worked through Explore and Plan, now in the phase code
@@ -74,7 +76,10 @@ publish action to the repo's GitHub Pages), an issue form for content errors, LI
74
76
  Renovate preset of lernapps). The installed skills and skills-lock.json are committed with the app, like the
75
77
  content guide; lernapps update replaces the lernapps-* skills.
76
78
 
77
- Then: git init (if <dir> is not a repository yet), npm install (installs the git hooks), and the first commit.
79
+ It installs nothing and commits nothing. Its last lines say what to do next, in this order: git init
80
+ --initial-branch=work (only if <dir> is not a repository yet; lernapps init made one), npm install (installs the
81
+ runtime and the git hooks), git add -A and git commit (the first commit, on work; the hooks check it), then follow
82
+ the plan.
78
83
 
79
84
  Exit codes: 0 when the app is written, 1 when it cannot be (the message says why), 2 on a usage error.
80
85
 
@@ -84,8 +89,8 @@ Options:
84
89
  its file name <NN>-<short name>.md, else 01-plan.md (default: the one in <dir>)
85
90
  --templates <dir> the package @lernapps/app-templates to copy from: its folder, or a clone of its repository
86
91
  (default: the environment variable LERNAPPS_TEMPLATES, else the installed package)
87
- --tooling <spec> the npm spec of @lernapps/tooling the app depends on (default: ^ the version of this
88
- @lernapps/tooling)
92
+ --tooling <spec> the npm spec of @lernapps/tooling the app depends on (default: the one lernapps init
93
+ installed, else ^ the version of this @lernapps/tooling)
89
94
  -h, --help print this help
90
95
  `;
91
96
 
@@ -98,6 +103,7 @@ export const fromPackage = (...path: string[]) => join(packageRoot, ...path);
98
103
  export interface Manifest {
99
104
  name?: string;
100
105
  version?: string;
106
+ dependencies?: Record<string, string>;
101
107
  devDependencies?: Record<string, string>;
102
108
  scripts?: Record<string, string>;
103
109
  lernapps?: { archetypes?: string[]; scaffold?: Record<string, string[]>; init?: boolean };
@@ -147,6 +153,19 @@ export function templatesPackage(dir?: string): Templates {
147
153
  };
148
154
  }
149
155
 
156
+ /** The version of a package built from a commit, not released: its package.json in the repository says 0.0.0. */
157
+ const UNRELEASED = "0.0.0";
158
+
159
+ /**
160
+ * The spec of the runtime an app depends on, for templates of `version`: ^ that version; for templates built from a
161
+ * commit (version 0.0.0, which the registry does not have) the spec this tooling depends on, the same commit
162
+ * (github:lernapps/app-templates#<commit>), so that template and runtime match.
163
+ */
164
+ export function runtimeSpec(version: string): string {
165
+ if (version !== UNRELEASED) return `^${version}`;
166
+ return readJson(fromPackage("package.json")).dependencies?.[RUNTIME] ?? `^${version}`;
167
+ }
168
+
150
169
  /** The archetypes of the installed templates. */
151
170
  export const archetypes = (): string[] => templatesPackage().archetypes;
152
171
 
@@ -241,6 +260,9 @@ export function writeScaffold(templates: Templates, archetype: string, dir: stri
241
260
  cpSync(start, dir, { recursive: true });
242
261
  }
243
262
 
263
+ /** The branch the plan works on (rule 8 of the plan template). */
264
+ export const WORK_BRANCH = "work";
265
+
244
266
  /** This package, as an app depends on it. */
245
267
  export const TOOLING = "@lernapps/tooling";
246
268
 
@@ -453,7 +475,7 @@ export function create(args: string[]): number {
453
475
  // the template uses the runtime of its own checkout (file:.., installed as a copy through .npmrc); the app the
454
476
  // version of the runtime it was copied from
455
477
  if (app.devDependencies?.[RUNTIME] !== undefined) {
456
- app.devDependencies = { ...app.devDependencies, [RUNTIME]: `^${templates.version}` };
478
+ app.devDependencies = { ...app.devDependencies, [RUNTIME]: runtimeSpec(templates.version) };
457
479
  }
458
480
  // the template keeps its checkout's runtime with an override (file:..), which npm would otherwise swap for the
459
481
  // version this tooling depends on; the app has no checkout to point at
@@ -463,8 +485,9 @@ export function create(args: string[]): number {
463
485
  if (Object.keys(kept).length > 0) app["overrides"] = kept;
464
486
  else delete app["overrides"];
465
487
  }
466
- // the tooling the app depends on: the one named, else this version (init may have installed it already)
467
- const tooling = values.tooling ?? `^${ownVersion()}`;
488
+ // the tooling the app depends on: the one named, else the one init installed (maybe a local one, --tooling of
489
+ // init), else this version
490
+ const tooling = values.tooling ?? initialised?.devDependencies?.[TOOLING] ?? `^${ownVersion()}`;
468
491
  app.devDependencies = {
469
492
  ...app.devDependencies,
470
493
  ...ARCHETYPE_DEV_DEPENDENCIES[archetype],
@@ -490,7 +513,13 @@ export function create(args: string[]): number {
490
513
  `lernapps create: the ${archetype} app ${name} in ${dir} (app-templates ${templates.version})`,
491
514
  ...guide.map((line) => `Content guide: ${line}`),
492
515
  ...(moved ? [`Plan: moved ${LEGACY_PLAN} to ${relative(dir, plan.target)}`] : []),
493
- `Next: ${existsSync(join(dir, ".git")) ? "" : "git init, "}npm install, then follow ${relative(dir, plan.target)}`,
516
+ "Next, in this order (create installs and commits nothing):",
517
+ ...[
518
+ ...(existsSync(join(dir, ".git")) ? [] : [`git init --initial-branch=${WORK_BRANCH}`]),
519
+ "npm install (the runtime and the git hooks)",
520
+ "git add -A, then git commit (the first commit; the hooks check it)",
521
+ `follow ${relative(dir, plan.target)}`,
522
+ ].map((step, index) => ` ${index + 1}. ${step}`),
494
523
  "",
495
524
  ].join("\n"),
496
525
  );
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.3.1",
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",
@@ -115,7 +115,7 @@
115
115
  },
116
116
  "devDependencies": {
117
117
  "@doctc/arc42": "0.38.0",
118
- "@lernapps/site": "github:lernapps/lernapps.github.io#8b6c98f76170c313519d04b38937a48747c05b09",
118
+ "@lernapps/site": "github:lernapps/lernapps.github.io#0f6257d3dffcbf69ffe962d0b80bd7298a62de02",
119
119
  "@types/node": "26.6.5",
120
120
  "ajv": "8.20.0",
121
121
  "vite-plus": "1.4.0"