@cosmicdrift/kumiko-cli 0.415.0 → 0.416.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-cli",
3
- "version": "0.415.0",
3
+ "version": "0.416.0",
4
4
  "description": "CLI for Kumiko apps. `bunx @cosmicdrift/kumiko-cli new app <name>` scaffolds a runnable workspace; `add feature <name>` mounts a feature; `check` runs boot validation plus the guard suites with the step list derived from kumiko.json; `agent lint`, `project` and `consumer` operate on an existing app; `handbuch build` renders the HTML handbuch.",
5
5
  "license": "MIT",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -36,19 +36,19 @@
36
36
  }
37
37
  },
38
38
  "dependencies": {
39
- "@cosmicdrift/kumiko-dev-server": "0.415.0",
40
- "@cosmicdrift/kumiko-feature-ast": "0.415.0",
41
- "@cosmicdrift/kumiko-guards": "0.415.0",
42
- "@cosmicdrift/kumiko-server-runtime": "0.415.0",
43
- "@cosmicdrift/kumiko-testing": "0.415.0",
39
+ "@cosmicdrift/kumiko-dev-server": "0.416.0",
40
+ "@cosmicdrift/kumiko-feature-ast": "0.416.0",
41
+ "@cosmicdrift/kumiko-guards": "0.416.0",
42
+ "@cosmicdrift/kumiko-server-runtime": "0.416.0",
43
+ "@cosmicdrift/kumiko-testing": "0.416.0",
44
44
  "marked": "^18.1.2",
45
45
  "zod": "^4.6.7"
46
46
  },
47
47
  "peerDependencies": {
48
48
  "sharp": "^0.35.5",
49
- "@cosmicdrift/kumiko-bundled-features": "0.415.0",
50
- "@cosmicdrift/kumiko-framework": "0.415.0",
51
- "@cosmicdrift/kumiko-renderer": "0.415.0"
49
+ "@cosmicdrift/kumiko-bundled-features": "0.416.0",
50
+ "@cosmicdrift/kumiko-framework": "0.416.0",
51
+ "@cosmicdrift/kumiko-renderer": "0.416.0"
52
52
  },
53
53
  "peerDependenciesMeta": {
54
54
  "sharp": {
@@ -65,9 +65,9 @@
65
65
  }
66
66
  },
67
67
  "devDependencies": {
68
- "@cosmicdrift/kumiko-bundled-features": "0.415.0",
69
- "@cosmicdrift/kumiko-framework": "0.415.0",
70
- "@cosmicdrift/kumiko-renderer": "0.415.0",
68
+ "@cosmicdrift/kumiko-bundled-features": "0.416.0",
69
+ "@cosmicdrift/kumiko-framework": "0.416.0",
70
+ "@cosmicdrift/kumiko-renderer": "0.416.0",
71
71
  "sharp": "^0.35.5"
72
72
  },
73
73
  "publishConfig": {
@@ -20,10 +20,23 @@ const BUILD_OPTIONS = {
20
20
  "no-large": { type: "boolean" },
21
21
  } as const;
22
22
 
23
- function usageError(ctx: CliCommandContext, message?: string): number {
23
+ const KNOWLEDGE_USAGE =
24
+ "Usage: kumiko handbuch knowledge <config.json> --out <file.json> [--root <dir>] [--texts <dir>] [--web-texts <dir>] [--web-repo <dir>] [--repo <dir>] [--locale <l>]";
25
+
26
+ const KNOWLEDGE_OPTIONS = {
27
+ root: { type: "string" },
28
+ out: { type: "string" },
29
+ texts: { type: "string" },
30
+ "web-texts": { type: "string" },
31
+ "web-repo": { type: "string" },
32
+ repo: { type: "string" },
33
+ locale: { type: "string" },
34
+ } as const;
35
+
36
+ function usageError(ctx: CliCommandContext, message?: string, usage: string = USAGE): number {
24
37
  ctx.out.err("");
25
38
  if (message) ctx.out.err(` ${message}`);
26
- ctx.out.err(` ${USAGE}`);
39
+ ctx.out.err(` ${usage}`);
27
40
  ctx.out.err("");
28
41
  return 2;
29
42
  }
@@ -73,31 +86,23 @@ function wipesBuildInputs({
73
86
  return wipesProtectedDir || lieInsideSource;
74
87
  }
75
88
 
76
- async function runBuild(ctx: CliCommandContext, args: ReadonlyArray<string>): Promise<number> {
77
- let parsed: ReturnType<
78
- typeof parseArgs<{ options: typeof BUILD_OPTIONS; allowPositionals: true }>
79
- >;
80
- try {
81
- parsed = parseArgs({ args: [...args], options: BUILD_OPTIONS, allowPositionals: true });
82
- } catch (error) {
83
- return usageError(ctx, describeError(error));
84
- }
85
- const { values, positionals } = parsed;
86
- const configArg = positionals[0];
87
- if (!configArg) return usageError(ctx, "Missing <config.json>.");
89
+ type ConfigOverrides = { locale?: string; repo?: string; "web-repo"?: string };
88
90
 
89
- const rootDir = resolve(ctx.cwd, values.root ?? ".");
90
- const configPath = resolve(rootDir, configArg);
91
+ // Reports a bad config itself; the caller only maps undefined to exit code 2.
92
+ async function loadConfigWithOverrides(
93
+ ctx: CliCommandContext,
94
+ configPath: string,
95
+ values: ConfigOverrides,
96
+ usage: string,
97
+ ): Promise<HandbuchConfig | undefined> {
91
98
  // Lazy: the builder pulls in optional peers (framework, headless) that other commands must not need.
92
99
  const { loadHandbuchConfig, restrictToLocale } = await import("../handbuch/config");
93
- const { buildHandbuch, SHARP_MISSING_MESSAGE } = await import("../handbuch/build");
94
- let config: HandbuchConfig;
95
100
  try {
96
101
  const loaded = loadHandbuchConfig(configPath);
97
102
  const narrowed = values.locale ? restrictToLocale(loaded, values.locale) : loaded;
98
103
  // Worktree checkouts hold the code the texts describe; the configured
99
104
  // sibling checkouts may sit on another commit.
100
- config = {
105
+ return {
101
106
  ...narrowed,
102
107
  repo: values.repo ? resolve(ctx.cwd, values.repo) : narrowed.repo,
103
108
  flowSources: narrowed.flowSources?.map((source) => ({
@@ -109,10 +114,71 @@ async function runBuild(ctx: CliCommandContext, args: ReadonlyArray<string>): Pr
109
114
  ctx.out.err("");
110
115
  ctx.out.err(` Invalid handbuch config ${configPath}:`);
111
116
  ctx.out.err(` ${describeError(error)}`);
112
- ctx.out.err(` ${USAGE}`);
117
+ ctx.out.err(` ${usage}`);
113
118
  ctx.out.err("");
114
- return 2;
119
+ return undefined;
115
120
  }
121
+ }
122
+
123
+ async function runKnowledge(ctx: CliCommandContext, args: ReadonlyArray<string>): Promise<number> {
124
+ let parsed: ReturnType<
125
+ typeof parseArgs<{ options: typeof KNOWLEDGE_OPTIONS; allowPositionals: true }>
126
+ >;
127
+ try {
128
+ parsed = parseArgs({ args: [...args], options: KNOWLEDGE_OPTIONS, allowPositionals: true });
129
+ } catch (error) {
130
+ return usageError(ctx, describeError(error), KNOWLEDGE_USAGE);
131
+ }
132
+ const { values, positionals } = parsed;
133
+ const configArg = positionals[0];
134
+ if (!configArg) return usageError(ctx, "Missing <config.json>.", KNOWLEDGE_USAGE);
135
+ if (!values.out) return usageError(ctx, "Missing --out <file>.", KNOWLEDGE_USAGE);
136
+
137
+ const rootDir = resolve(ctx.cwd, values.root ?? ".");
138
+ const configPath = resolve(rootDir, configArg);
139
+ const config = await loadConfigWithOverrides(ctx, configPath, values, KNOWLEDGE_USAGE);
140
+ if (!config) return 2;
141
+
142
+ const { writeKnowledge } = await import("../handbuch/knowledge");
143
+ try {
144
+ const result = await writeKnowledge(config, {
145
+ rootDir,
146
+ outFile: resolve(ctx.cwd, values.out),
147
+ textsDir: values.texts ? resolve(ctx.cwd, values.texts) : undefined,
148
+ flowTextsDir: values["web-texts"] ? resolve(ctx.cwd, values["web-texts"]) : undefined,
149
+ });
150
+ const counts = Object.entries(result.pageCounts)
151
+ .map(([locale, count]) => `${locale} ${count}`)
152
+ .join(", ");
153
+ ctx.out.log(`${config.title}: ${counts} Seiten`);
154
+ ctx.out.log(result.outFile);
155
+ return 0;
156
+ } catch (error) {
157
+ ctx.out.err("");
158
+ ctx.out.err(` ✗ ${describeError(error)}`);
159
+ ctx.out.err("");
160
+ return 1;
161
+ }
162
+ }
163
+
164
+ async function runBuild(ctx: CliCommandContext, args: ReadonlyArray<string>): Promise<number> {
165
+ let parsed: ReturnType<
166
+ typeof parseArgs<{ options: typeof BUILD_OPTIONS; allowPositionals: true }>
167
+ >;
168
+ try {
169
+ parsed = parseArgs({ args: [...args], options: BUILD_OPTIONS, allowPositionals: true });
170
+ } catch (error) {
171
+ return usageError(ctx, describeError(error));
172
+ }
173
+ const { values, positionals } = parsed;
174
+ const configArg = positionals[0];
175
+ if (!configArg) return usageError(ctx, "Missing <config.json>.");
176
+
177
+ const rootDir = resolve(ctx.cwd, values.root ?? ".");
178
+ const configPath = resolve(rootDir, configArg);
179
+ const { SHARP_MISSING_MESSAGE, buildHandbuch } = await import("../handbuch/build");
180
+ const config = await loadConfigWithOverrides(ctx, configPath, values, USAGE);
181
+ if (!config) return 2;
116
182
 
117
183
  // buildHandbuch wipes outDir recursively, so a mistyped --out must never be
118
184
  // (or contain) a directory the build reads from or the user works in.
@@ -162,10 +228,12 @@ async function runBuild(ctx: CliCommandContext, args: ReadonlyArray<string>): Pr
162
228
 
163
229
  export const handbuchCommand: CliCommand = {
164
230
  id: "handbuch",
165
- description: "Build the HTML handbuch from a config (build <config.json>)",
166
- help: `${USAGE}\n\nRenders the handbuch page from the texts and screenshots a config points at.\nRelative paths in the config resolve against --root (default: cwd); the output\ngoes to <root>/.handbuch/<config-name> unless --out is given. Needs sharp\n(bun add -d sharp) for the image conversion.`,
231
+ description:
232
+ "Build the HTML handbuch (build <config.json>) or export its texts as JSON (knowledge <config.json>)",
233
+ help: `${USAGE}\n${KNOWLEDGE_USAGE}\n\nknowledge writes the page texts per locale (without images) as deterministic JSON for the assistant.\n\nRenders the handbuch page from the texts and screenshots a config points at.\nRelative paths in the config resolve against --root (default: cwd); the output\ngoes to <root>/.handbuch/<config-name> unless --out is given. Needs sharp\n(bun add -d sharp) for the image conversion.`,
167
234
  run: async (ctx) => {
168
235
  const [sub, ...rest] = ctx.argv;
236
+ if (sub === "knowledge") return runKnowledge(ctx, rest);
169
237
  if (sub !== "build") return usageError(ctx);
170
238
  return runBuild(ctx, rest);
171
239
  },
@@ -21,6 +21,7 @@ export type Chrome = {
21
21
  lightbox: string;
22
22
  close: string;
23
23
  fallbackGroup: string;
24
+ processGroup: string;
24
25
  builtAt: string;
25
26
  viewports: { desktop: string; tablet: string; mobile: string; all: string };
26
27
  };
@@ -48,6 +49,7 @@ const de: Chrome = {
48
49
  lightbox: "Screenshot vergrößert",
49
50
  close: "Schließen",
50
51
  fallbackGroup: "Weitere",
52
+ processGroup: "Aufgaben",
51
53
  builtAt: "Stand",
52
54
  viewports: {
53
55
  desktop: "Desktop",
@@ -80,6 +82,7 @@ const en: Chrome = {
80
82
  lightbox: "Screenshot enlarged",
81
83
  close: "Close",
82
84
  fallbackGroup: "Other",
85
+ processGroup: "Tasks",
83
86
  builtAt: "Built",
84
87
  viewports: {
85
88
  desktop: "Desktop",
@@ -50,6 +50,7 @@ export const handbuchConfigSchema = z
50
50
  groups: z.array(groupSchema).min(1),
51
51
  translations: z.record(z.string(), translationsSchema).optional(),
52
52
  textsDir: z.string().includes("{locale}"),
53
+ processTextsDir: z.string().includes("{locale}").optional(),
53
54
  shotsDir: z.string().optional(),
54
55
  uiStrings: z.array(z.string().min(1)).optional(),
55
56
  uiStringPackages: z
@@ -0,0 +1,82 @@
1
+ import { mkdirSync, writeFileSync } from "node:fs";
2
+ import { dirname } from "node:path";
3
+ import type { HandbuchConfig } from "./config";
4
+ import { type BuildModelOptions, buildModel, type Chapter, type Model } from "./model";
5
+
6
+ export type KnowledgePageKind = "process" | "screen";
7
+
8
+ export type KnowledgePage = {
9
+ id: string;
10
+ kind: KnowledgePageKind;
11
+ title: string;
12
+ questions: string[];
13
+ screens: string[];
14
+ start?: string;
15
+ bodyMd: string;
16
+ };
17
+
18
+ export type KnowledgeFile = {
19
+ defaultLocale: string;
20
+ locales: Record<string, KnowledgePage[]>;
21
+ };
22
+
23
+ const MARKDOWN_IMAGE = /!\[[^\]]*\]\([^)]*\)/g;
24
+
25
+ export function stripMarkdownImages(bodyMd: string): string {
26
+ return bodyMd
27
+ .replace(MARKDOWN_IMAGE, "")
28
+ .replace(/[ \t]+$/gm, "")
29
+ .replace(/\n{3,}/g, "\n\n")
30
+ .trim();
31
+ }
32
+
33
+ function compareText(a: string, b: string): number {
34
+ if (a < b) return -1;
35
+ return a > b ? 1 : 0;
36
+ }
37
+
38
+ function pageOf(chapter: Chapter, locale: string): KnowledgePage | undefined {
39
+ const text = chapter.texts.get(locale);
40
+ if (!text) return undefined;
41
+ const kind: KnowledgePageKind = chapter.isProcess ? "process" : "screen";
42
+ return {
43
+ id: chapter.id,
44
+ kind,
45
+ title: text.title,
46
+ questions: text.questions,
47
+ screens: kind === "process" ? text.screens : [chapter.id],
48
+ ...(kind === "process" && text.start !== undefined && { start: text.start }),
49
+ bodyMd: stripMarkdownImages(text.bodyMd),
50
+ };
51
+ }
52
+
53
+ function comparePages(a: KnowledgePage, b: KnowledgePage): number {
54
+ return compareText(a.kind, b.kind) || compareText(a.id, b.id);
55
+ }
56
+
57
+ export function buildKnowledge(model: Model): KnowledgeFile {
58
+ const chapters = model.groups.flatMap((group) => group.chapters);
59
+ const locales: Record<string, KnowledgePage[]> = {};
60
+ for (const locale of [...model.locales].sort(compareText)) {
61
+ locales[locale] = chapters
62
+ .flatMap((chapter) => pageOf(chapter, locale) ?? [])
63
+ .sort(comparePages);
64
+ }
65
+ return { defaultLocale: model.defaultLocale, locales };
66
+ }
67
+
68
+ export async function writeKnowledge(
69
+ config: HandbuchConfig,
70
+ options: BuildModelOptions & { outFile: string },
71
+ ): Promise<{ outFile: string; pageCounts: Record<string, number> }> {
72
+ const { outFile, ...modelOptions } = options;
73
+ const knowledge = buildKnowledge(await buildModel(config, modelOptions));
74
+ mkdirSync(dirname(outFile), { recursive: true });
75
+ writeFileSync(outFile, `${JSON.stringify(knowledge, null, 2)}\n`);
76
+ return {
77
+ outFile,
78
+ pageCounts: Object.fromEntries(
79
+ Object.entries(knowledge.locales).map(([locale, pages]) => [locale, pages.length]),
80
+ ),
81
+ };
82
+ }
@@ -22,6 +22,8 @@ export const VIEWPORT_WIDTH: Record<Viewport, number> = {
22
22
  };
23
23
 
24
24
  const FALLBACK_GROUP_ID = "weitere";
25
+ const PROCESS_GROUP_ID = "aufgaben";
26
+ const PROCESS_ID_PREFIX = "prozess-";
25
27
 
26
28
  const LIGHT_THEME_DIR = "default-light";
27
29
  const LOCAL_IMAGE_REF = /\.(png|jpe?g|webp|gif|svg)$/i;
@@ -35,7 +37,10 @@ export type LocaleText = {
35
37
  bodyMd: string;
36
38
  dir: string;
37
39
  gruppe?: string;
40
+ start?: string;
38
41
  prozesse: string[];
42
+ questions: string[];
43
+ screens: string[];
39
44
  missingImages: string[];
40
45
  uiIgnore: string[];
41
46
  staleTerms: string[];
@@ -56,6 +61,7 @@ export type Chapter = {
56
61
  matrix: Map<string, MatrixShots>;
57
62
  steps?: Step[];
58
63
  inScreenList: boolean;
64
+ isProcess?: boolean;
59
65
  };
60
66
 
61
67
  export type Model = {
@@ -134,6 +140,24 @@ export function parseList(raw: string | undefined): string[] {
134
140
  .filter(Boolean);
135
141
  }
136
142
 
143
+ // Inline `key: [a, b]` or a YAML block list; parseFrontmatter only keeps single-line values.
144
+ export function parseFrontmatterList(raw: string, key: string): string[] {
145
+ const block = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/)?.[1];
146
+ if (block === undefined) return [];
147
+ const lines = block.split(/\r?\n/);
148
+ const start = lines.findIndex((line) => line.startsWith(`${key}:`));
149
+ if (start === -1) return [];
150
+ const inline = (lines[start] ?? "").slice(key.length + 1).trim();
151
+ if (inline) return parseList(inline);
152
+ const items: string[] = [];
153
+ for (const line of lines.slice(start + 1)) {
154
+ const item = line.match(/^\s*-\s+(.*)$/)?.[1];
155
+ if (item === undefined) break;
156
+ items.push(item.trim().replace(/^["']|["']$/g, ""));
157
+ }
158
+ return items;
159
+ }
160
+
137
161
  export function stripLeadingHeading(body: string): string {
138
162
  const lines = body.split(/\r?\n/);
139
163
  let index = 0;
@@ -170,7 +194,8 @@ export function readTexts(textsRoot: string): Map<string, LocaleText & { id: str
170
194
  for (const file of readdirSync(textsRoot)
171
195
  .filter((name) => name.endsWith(".md"))
172
196
  .sort()) {
173
- const { data, body } = parseFrontmatter(readFileSync(join(textsRoot, file), "utf8"));
197
+ const raw = readFileSync(join(textsRoot, file), "utf8");
198
+ const { data, body } = parseFrontmatter(raw);
174
199
  const id = data["screenId"] || basename(file, ".md");
175
200
  const bodyMd = stripLeadingHeading(body);
176
201
  texts.set(id, {
@@ -179,7 +204,10 @@ export function readTexts(textsRoot: string): Map<string, LocaleText & { id: str
179
204
  bodyMd,
180
205
  dir: textsRoot,
181
206
  gruppe: data["gruppe"] || undefined,
207
+ start: data["start"] || undefined,
182
208
  prozesse: parseList(data["prozesse"]),
209
+ questions: parseFrontmatterList(raw, "fragen"),
210
+ screens: parseFrontmatterList(raw, "masken"),
183
211
  uiIgnore: parseList(data["ui-ignore"]),
184
212
  staleTerms: [],
185
213
  missingImages: localImageRefs(bodyMd).filter(
@@ -190,6 +218,16 @@ export function readTexts(textsRoot: string): Map<string, LocaleText & { id: str
190
218
  return texts;
191
219
  }
192
220
 
221
+ // Prefixed so a process page never shares an id with a screen chapter.
222
+ function readProcessTexts(textsRoot: string): Map<string, LocaleText & { id: string }> {
223
+ const processTexts = new Map<string, LocaleText & { id: string }>();
224
+ for (const [rawId, text] of readTexts(textsRoot)) {
225
+ const id = `${PROCESS_ID_PREFIX}${rawId}`;
226
+ processTexts.set(id, { ...text, id, gruppe: PROCESS_GROUP_ID });
227
+ }
228
+ return processTexts;
229
+ }
230
+
193
231
  export function discoverMatrix(
194
232
  shotsDir: string,
195
233
  locales: readonly string[],
@@ -395,19 +433,12 @@ function registerChapters(
395
433
  return { chapters, sourceOrder, configGroupOf };
396
434
  }
397
435
 
398
- export async function buildModel(
436
+ function loadTextsByLocale(
399
437
  config: HandbuchConfig,
400
438
  options: BuildModelOptions,
401
- ): Promise<Model> {
402
- const repoDir = resolve(options.rootDir, config.repo);
403
- const screens =
404
- options.screens ??
405
- config.screens ??
406
- (config.screensModule
407
- ? await loadScreensModule(options.rootDir, config.repo, config.screensModule)
408
- : []);
409
-
410
- const textsByLocale = new Map(
439
+ repoDir: string,
440
+ ): Map<string, Map<string, LocaleText>> {
441
+ return new Map(
411
442
  config.locales.map((locale) => [
412
443
  locale,
413
444
  readTexts(
@@ -417,19 +448,63 @@ export async function buildModel(
417
448
  ),
418
449
  ]),
419
450
  );
420
- const applyStale = (
421
- byLocale: Map<string, Map<string, LocaleText>>,
422
- uiStrings: Map<string, string[]>,
423
- ) => {
424
- const uiIndex = buildUiIndex(uiStrings);
425
- for (const [locale, texts] of byLocale) {
426
- for (const text of texts.values()) {
427
- text.staleTerms = findMissingUiTerms(text.bodyMd, locale, uiIndex, text.uiIgnore);
428
- }
451
+ }
452
+
453
+ function applyStaleTerms(
454
+ byLocale: Map<string, Map<string, LocaleText>>,
455
+ uiStrings: Map<string, string[]>,
456
+ ): void {
457
+ const uiIndex = buildUiIndex(uiStrings);
458
+ for (const [locale, texts] of byLocale) {
459
+ for (const text of texts.values()) {
460
+ text.staleTerms = findMissingUiTerms(text.bodyMd, locale, uiIndex, text.uiIgnore);
429
461
  }
430
- };
462
+ }
463
+ }
464
+
465
+ function mergeProcessTexts(
466
+ config: HandbuchConfig,
467
+ repoDir: string,
468
+ textsByLocale: Map<string, Map<string, LocaleText>>,
469
+ ): Set<string> {
470
+ const processIds = new Set<string>();
471
+ const processTextsDir = config.processTextsDir;
472
+ if (!processTextsDir) return processIds;
473
+ for (const locale of config.locales) {
474
+ const processTexts = readProcessTexts(
475
+ join(repoDir, processTextsDir.replace("{locale}", locale)),
476
+ );
477
+ for (const [id, text] of processTexts) {
478
+ textsByLocale.get(locale)?.set(id, text);
479
+ processIds.add(id);
480
+ }
481
+ }
482
+ return processIds;
483
+ }
484
+
485
+ function markProcessChapters(chapters: Map<string, Chapter>, processIds: Set<string>): void {
486
+ for (const id of processIds) {
487
+ const chapter = chapters.get(id);
488
+ if (chapter) chapter.isProcess = true;
489
+ }
490
+ }
491
+
492
+ export async function buildModel(
493
+ config: HandbuchConfig,
494
+ options: BuildModelOptions,
495
+ ): Promise<Model> {
496
+ const repoDir = resolve(options.rootDir, config.repo);
497
+ const screens =
498
+ options.screens ??
499
+ config.screens ??
500
+ (config.screensModule
501
+ ? await loadScreensModule(options.rootDir, config.repo, config.screensModule)
502
+ : []);
503
+
504
+ const textsByLocale = loadTextsByLocale(config, options, repoDir);
505
+ const processIds = mergeProcessTexts(config, repoDir, textsByLocale);
431
506
  if (config.uiStrings || config.uiStringPackages) {
432
- applyStale(
507
+ applyStaleTerms(
433
508
  textsByLocale,
434
509
  collectUiStrings({
435
510
  repoDir,
@@ -445,6 +520,10 @@ export async function buildModel(
445
520
  ...group,
446
521
  label: tr?.groups[group.id] ?? group.label,
447
522
  });
523
+ const processGroups: Group[] =
524
+ processIds.size > 0 && !config.groups.some((group) => group.id === PROCESS_GROUP_ID)
525
+ ? [labelGroup({ id: PROCESS_GROUP_ID, label: chromeFor(config.defaultLocale).processGroup })]
526
+ : [];
448
527
  const fallbackGroupDefault: Group = {
449
528
  id: FALLBACK_GROUP_ID,
450
529
  label: chromeFor(config.defaultLocale).fallbackGroup,
@@ -454,7 +533,7 @@ export async function buildModel(
454
533
  config,
455
534
  options,
456
535
  textsByLocale,
457
- applyStale,
536
+ applyStaleTerms,
458
537
  labelGroup,
459
538
  );
460
539
  const shotsDir =
@@ -463,13 +542,14 @@ export async function buildModel(
463
542
  ? discoverMatrix(shotsDir, config.locales)
464
543
  : new Map<string, Map<string, MatrixShots>>();
465
544
 
466
- const groups: Group[] = [...config.groups.map(labelGroup), ...flowGroups];
545
+ const groups: Group[] = [...config.groups.map(labelGroup), ...flowGroups, ...processGroups];
467
546
  const { chapters, sourceOrder, configGroupOf } = registerChapters(
468
547
  screens,
469
548
  flowChapters,
470
549
  textsByLocale,
471
550
  matrix,
472
551
  );
552
+ markProcessChapters(chapters, processIds);
473
553
 
474
554
  const fallbackGroup = assignGroups(config, chapters, groups, configGroupOf, fallbackGroupDefault);
475
555
  return assembleModel(config, chapters, groups, sourceOrder, fallbackGroup);