akm-cli 0.9.0-beta.54 → 0.9.0-beta.55

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 (101) hide show
  1. package/dist/cli.js +5 -3
  2. package/dist/commands/agent/contribute-cli.js +2 -3
  3. package/dist/commands/env/env-cli.js +187 -202
  4. package/dist/commands/env/secret-cli.js +109 -121
  5. package/dist/commands/feedback-cli.js +152 -155
  6. package/dist/commands/health/advisories.js +151 -0
  7. package/dist/commands/health/improve-metrics.js +754 -0
  8. package/dist/commands/health/llm-usage.js +65 -0
  9. package/dist/commands/health/md-report.js +103 -0
  10. package/dist/commands/health/metrics.js +278 -0
  11. package/dist/commands/health/task-runs.js +135 -0
  12. package/dist/commands/health/types.js +18 -0
  13. package/dist/commands/health/windows.js +196 -0
  14. package/dist/commands/health.js +14 -1624
  15. package/dist/commands/improve/anti-collapse.js +170 -0
  16. package/dist/commands/improve/collapse-detector.js +3 -2
  17. package/dist/commands/improve/consolidate.js +636 -633
  18. package/dist/commands/improve/dedup.js +1 -1
  19. package/dist/commands/improve/distill/content-repair.js +202 -0
  20. package/dist/commands/improve/distill/promote-memory.js +228 -0
  21. package/dist/commands/improve/distill/quality-gate.js +233 -0
  22. package/dist/commands/improve/distill-guards.js +127 -0
  23. package/dist/commands/improve/distill.js +49 -575
  24. package/dist/commands/improve/extract-cli.js +74 -76
  25. package/dist/commands/improve/extract.js +6 -4
  26. package/dist/commands/improve/hot-probation.js +45 -0
  27. package/dist/commands/improve/improve-auto-accept.js +3 -2
  28. package/dist/commands/improve/improve-cli.js +14 -13
  29. package/dist/commands/improve/improve-result-file.js +2 -1
  30. package/dist/commands/improve/improve.js +6 -5
  31. package/dist/commands/improve/loop-stages.js +19 -21
  32. package/dist/commands/improve/preparation.js +4 -2
  33. package/dist/commands/improve/procedural.js +10 -31
  34. package/dist/commands/improve/recombine.js +19 -43
  35. package/dist/commands/improve/reflect.js +1 -1
  36. package/dist/commands/improve/schema-similarity-gate.js +168 -0
  37. package/dist/commands/improve/shared.js +48 -0
  38. package/dist/commands/observability-cli.js +4 -4
  39. package/dist/commands/proposal/drain-policies.js +2 -2
  40. package/dist/commands/proposal/drain.js +1 -1
  41. package/dist/commands/proposal/legacy-import.js +115 -0
  42. package/dist/commands/proposal/proposal-cli.js +3 -3
  43. package/dist/commands/proposal/proposal.js +2 -1
  44. package/dist/commands/proposal/propose.js +1 -1
  45. package/dist/commands/proposal/repository.js +829 -0
  46. package/dist/commands/proposal/validators/proposals.js +5 -920
  47. package/dist/commands/read/remember-cli.js +132 -137
  48. package/dist/commands/read/search-cli.js +1 -1
  49. package/dist/commands/registry-cli.js +76 -87
  50. package/dist/commands/sources/add-cli.js +90 -94
  51. package/dist/commands/sources/history.js +1 -1
  52. package/dist/commands/sources/schema-repair.js +1 -1
  53. package/dist/commands/sources/sources-cli.js +3 -3
  54. package/dist/commands/sources/stash-cli.js +1 -1
  55. package/dist/commands/tasks/tasks-cli.js +1 -2
  56. package/dist/commands/wiki-cli.js +2 -3
  57. package/dist/core/common.js +3 -3
  58. package/dist/core/config/config-schema.js +6 -0
  59. package/dist/core/deep-merge.js +38 -0
  60. package/dist/core/events.js +2 -1
  61. package/dist/core/logs-db.js +8 -13
  62. package/dist/core/paths.js +14 -14
  63. package/dist/core/state-db.js +13 -1140
  64. package/dist/indexer/db/db.js +66 -709
  65. package/dist/indexer/db/entry-mapper.js +41 -0
  66. package/dist/indexer/db/schema.js +516 -0
  67. package/dist/indexer/feedback/utility-policy.js +85 -0
  68. package/dist/indexer/graph/graph-extraction.js +2 -1
  69. package/dist/indexer/index-writer-lock.js +9 -0
  70. package/dist/indexer/indexer.js +78 -23
  71. package/dist/indexer/search/fts-query.js +51 -0
  72. package/dist/integrations/agent/spawn.js +15 -66
  73. package/dist/output/text/helpers.js +13 -0
  74. package/dist/scripts/migrate-storage.js +6891 -7436
  75. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +44 -43
  76. package/dist/setup/legacy-config.js +106 -0
  77. package/dist/setup/prompt.js +57 -0
  78. package/dist/setup/providers.js +14 -0
  79. package/dist/setup/semantic-assets.js +124 -0
  80. package/dist/setup/setup.js +24 -1607
  81. package/dist/setup/steps/connection.js +734 -0
  82. package/dist/setup/steps/output.js +31 -0
  83. package/dist/setup/steps/platforms.js +124 -0
  84. package/dist/setup/steps/semantic.js +27 -0
  85. package/dist/setup/steps/sources.js +222 -0
  86. package/dist/setup/steps/stashdir.js +42 -0
  87. package/dist/setup/steps/tasks.js +152 -0
  88. package/dist/storage/repositories/canaries-repository.js +107 -0
  89. package/dist/storage/repositories/consolidation-repository.js +38 -0
  90. package/dist/storage/repositories/embeddings-repository.js +72 -0
  91. package/dist/storage/repositories/events-repository.js +187 -0
  92. package/dist/storage/repositories/extract-sessions-repository.js +96 -0
  93. package/dist/storage/repositories/improve-runs-repository.js +130 -0
  94. package/dist/storage/repositories/index-db.js +4 -7
  95. package/dist/storage/repositories/proposals-repository.js +220 -0
  96. package/dist/storage/repositories/recombine-repository.js +213 -0
  97. package/dist/storage/repositories/task-history-repository.js +93 -0
  98. package/dist/storage/sqlite-pragmas.js +3 -3
  99. package/dist/tasks/runner.js +2 -1
  100. package/package.json +1 -1
  101. package/dist/commands/improve/homeostatic.js +0 -497
@@ -54,15 +54,19 @@ export async function acquireIndexWriterLease(options) {
54
54
  const maxWaitMs = options.maxWaitMs ?? DEFAULT_INDEX_WRITER_MAX_WAIT_MS;
55
55
  fs.mkdirSync(path.dirname(lockPath), { recursive: true });
56
56
  if (heldLocks.has(lockPath)) {
57
+ options.onAcquired?.({ waitedMs: 0 });
57
58
  return retainHeldLock(lockPath);
58
59
  }
60
+ let lastWaitNoticeMs = 0;
59
61
  while (true) {
60
62
  throwIfAborted(options.signal);
61
63
  if (tryAcquireLockSync(lockPath, buildPayload(options.purpose))) {
64
+ options.onAcquired?.({ waitedMs: Date.now() - startedAt });
62
65
  return retainHeldLock(lockPath);
63
66
  }
64
67
  const probe = probeLock(lockPath, { staleAfterMs: INDEX_WRITER_LOCK_STALE_AFTER_MS });
65
68
  if (probe.state === "held" && probe.holderPid === process.pid) {
69
+ options.onAcquired?.({ waitedMs: Date.now() - startedAt });
66
70
  return retainHeldLock(lockPath);
67
71
  }
68
72
  if (probe.state === "stale") {
@@ -77,6 +81,11 @@ export async function acquireIndexWriterLease(options) {
77
81
  if (maxWaitMs >= 0 && Date.now() - startedAt >= maxWaitMs) {
78
82
  throw new Error(`timed out waiting for index writer lease for ${options.purpose}`);
79
83
  }
84
+ const waitedMs = Date.now() - startedAt;
85
+ if (waitedMs - lastWaitNoticeMs >= 15000) {
86
+ options.onWait?.({ waitedMs });
87
+ lastWaitNoticeMs = waitedMs;
88
+ }
80
89
  await delay(INDEX_WRITER_WAIT_MS);
81
90
  }
82
91
  }
@@ -143,6 +143,7 @@ async function runEmbeddingPhase(ctx) {
143
143
  */
144
144
  async function runFinalizePhase(ctx) {
145
145
  const { db, config, sources, sourceDirs, isIncremental, stashDir, signal, onProgress } = ctx;
146
+ ctx.timing.tFinalizeStart = Date.now();
146
147
  // Rebuild FTS after all inserts. Use incremental mode when this whole
147
148
  // index run is incremental — only entries touched by `upsertEntry`
148
149
  // since the last rebuild are re-indexed.
@@ -153,10 +154,13 @@ async function runFinalizePhase(ctx) {
153
154
  });
154
155
  ctx.timing.tFtsEnd = Date.now();
155
156
  // Re-link detached usage_events and recompute utility scores.
157
+ onProgress({ phase: "finalize", message: "Relinking usage events." });
156
158
  relinkUsageEvents(db);
159
+ onProgress({ phase: "finalize", message: "Recomputing utility scores." });
157
160
  recomputeUtilityScores(db);
158
161
  // Purge LLM cache entries for assets that no longer exist in the index.
159
162
  try {
163
+ onProgress({ phase: "finalize", message: "Clearing stale LLM cache entries." });
160
164
  clearStaleCacheEntries(db);
161
165
  }
162
166
  catch {
@@ -164,6 +168,7 @@ async function runFinalizePhase(ctx) {
164
168
  }
165
169
  // Regenerate each wiki's index.md from its pages' frontmatter. Best-effort.
166
170
  try {
171
+ onProgress({ phase: "finalize", message: "Regenerating wiki indexes." });
167
172
  const { regenerateAllWikiIndexes } = await import("../wiki/wiki.js");
168
173
  regenerateAllWikiIndexes(stashDir);
169
174
  }
@@ -180,6 +185,7 @@ async function runFinalizePhase(ctx) {
180
185
  warnIfVecMissing(db);
181
186
  const totalEntries = getEntryCount(db);
182
187
  const semanticEntryCount = getEmbeddableEntryCount(db);
188
+ onProgress({ phase: "finalize", message: "Verifying semantic search state." });
183
189
  const verification = verifyIndexState(db, config, semanticEntryCount, embeddingResult);
184
190
  if (config.semanticSearchMode === "off") {
185
191
  clearSemanticStatus();
@@ -199,6 +205,7 @@ async function runFinalizePhase(ctx) {
199
205
  // Store verification result and totalEntries on ctx for the caller to use
200
206
  ctx.verification = verification;
201
207
  ctx.totalEntries = totalEntries;
208
+ ctx.timing.tFinalizeEnd = Date.now();
202
209
  // suppress unused warning — sources was previously used inline
203
210
  void sources;
204
211
  }
@@ -239,7 +246,21 @@ export async function akmIndex(options) {
239
246
  return akmIndexReal(options);
240
247
  }
241
248
  async function akmIndexReal(options) {
242
- return withIndexWriterLease({ purpose: "akm-index", signal: options?.signal }, async () => {
249
+ const requestedAt = Date.now();
250
+ let acquiredAt = requestedAt;
251
+ return withIndexWriterLease({
252
+ purpose: "akm-index",
253
+ signal: options?.signal,
254
+ onWait: ({ waitedMs }) => {
255
+ options?.onProgress?.({
256
+ phase: "preflight",
257
+ message: `Waiting for index writer lease (${Math.round(waitedMs / 1000)}s elapsed).`,
258
+ });
259
+ },
260
+ onAcquired: ({ waitedMs }) => {
261
+ acquiredAt = requestedAt + waitedMs;
262
+ },
263
+ }, async () => {
243
264
  const stashDir = options?.stashDir || resolveStashDir();
244
265
  const onProgress = options?.onProgress ?? (() => { });
245
266
  const signal = options?.signal;
@@ -258,10 +279,17 @@ async function akmIndexReal(options) {
258
279
  warnOnUnmigratedVaults(stashDir);
259
280
  // Ensure git stash caches are extracted before resolving stash dirs,
260
281
  // so their content directories exist on disk for the walker to discover.
282
+ const sourceCacheStart = Date.now();
283
+ onProgress({ phase: "preflight", message: "Hydrating source caches." });
261
284
  const { ensureSourceCaches, resolveSourceEntries } = await import("./search/search-source.js");
262
285
  await ensureSourceCaches(config, { force: full });
286
+ const sourceCacheEnd = Date.now();
263
287
  const allSourceEntries = resolveSourceEntries(stashDir, config);
264
288
  const allSourceDirs = allSourceEntries.map((s) => s.path);
289
+ onProgress({
290
+ phase: "preflight",
291
+ message: `Resolved ${allSourceDirs.length} stash source${allSourceDirs.length === 1 ? "" : "s"}.`,
292
+ });
265
293
  const t0 = Date.now();
266
294
  // Open database — pass embedding dimension from config if available
267
295
  const dbPath = getDbPath();
@@ -291,6 +319,8 @@ async function akmIndexReal(options) {
291
319
  tLlmEnd: t0,
292
320
  tFtsEnd: t0,
293
321
  tEmbedEnd: t0,
322
+ tFinalizeStart: t0,
323
+ tFinalizeEnd: t0,
294
324
  },
295
325
  isIncremental,
296
326
  builtAtMs,
@@ -328,9 +358,15 @@ async function akmIndexReal(options) {
328
358
  // After the normal index completes, remove entries whose source files no
329
359
  // longer exist on disk. Remote entries (empty file_path) are skipped.
330
360
  let cleanResult;
361
+ const cleanStart = Date.now();
331
362
  if (clean) {
363
+ onProgress({
364
+ phase: "finalize",
365
+ message: dryRun ? "Scanning for stale index entries (dry run)." : "Removing stale index entries.",
366
+ });
332
367
  cleanResult = runCleanPass(db, dryRun);
333
368
  }
369
+ const cleanEnd = Date.now();
334
370
  // ────────────────────────────────────────────────────────────────────────
335
371
  return {
336
372
  stashDir,
@@ -348,6 +384,12 @@ async function akmIndexReal(options) {
348
384
  llmMs: timing.tLlmEnd - timing.tWalkEnd,
349
385
  embedMs: timing.tEmbedEnd - timing.tLlmEnd,
350
386
  ftsMs: timing.tFtsEnd - timing.tEmbedEnd,
387
+ finalizeMs: timing.tFinalizeEnd - timing.tFinalizeStart,
388
+ cleanMs: clean ? cleanEnd - cleanStart : 0,
389
+ preflightMs: timing.t0 - requestedAt,
390
+ leaseWaitMs: acquiredAt - requestedAt,
391
+ sourceCacheMs: sourceCacheEnd - sourceCacheStart,
392
+ endToEndMs: Date.now() - requestedAt,
351
393
  },
352
394
  ...(cleanResult !== undefined ? { clean: cleanResult } : {}),
353
395
  };
@@ -854,31 +896,44 @@ async function generateEmbeddingsForDb(db, config, onProgress, signal) {
854
896
  warnVerbose(`[embed] ${ref} (${chars} chars, est. ${tokens} tokens) → batch ${batchNum}/${totalBatches}`);
855
897
  }
856
898
  }
857
- const embeddings = await embedBatch(texts, config.embedding, signal);
858
- throwIfAborted(signal);
859
- // Wrap all embedding upserts in a single transaction so partial
860
- // state is rolled back on failure rather than leaving the table half-filled.
861
- let storedCount = 0;
862
- let skippedCount = 0;
863
- db.transaction(() => {
864
- for (let i = 0; i < allEntries.length; i++) {
865
- if (upsertEmbedding(db, allEntries[i].id, embeddings[i])) {
866
- storedCount++;
867
- }
868
- else {
869
- skippedCount++;
899
+ let heartbeatTimer;
900
+ try {
901
+ heartbeatTimer = setInterval(() => {
902
+ onProgress({
903
+ phase: "embeddings",
904
+ message: `Still generating embeddings for ${allEntries.length} entr${allEntries.length === 1 ? "y" : "ies"}; waiting on embedding provider.`,
905
+ });
906
+ }, 15000);
907
+ const embeddings = await embedBatch(texts, config.embedding, signal);
908
+ throwIfAborted(signal);
909
+ // Wrap all embedding upserts in a single transaction so partial
910
+ // state is rolled back on failure rather than leaving the table half-filled.
911
+ let storedCount = 0;
912
+ let skippedCount = 0;
913
+ db.transaction(() => {
914
+ for (let i = 0; i < allEntries.length; i++) {
915
+ if (upsertEmbedding(db, allEntries[i].id, embeddings[i])) {
916
+ storedCount++;
917
+ }
918
+ else {
919
+ skippedCount++;
920
+ }
870
921
  }
922
+ })();
923
+ if (skippedCount > 0) {
924
+ warn(`[embed] ${skippedCount} embedding${skippedCount === 1 ? "" : "s"} skipped (entry deleted between queue and write)`);
871
925
  }
872
- })();
873
- if (skippedCount > 0) {
874
- warn(`[embed] ${skippedCount} embedding${skippedCount === 1 ? "" : "s"} skipped (entry deleted between queue and write)`);
926
+ onProgress({
927
+ phase: "embeddings",
928
+ message: `Stored ${storedCount} embedding${storedCount === 1 ? "" : "s"}.`,
929
+ });
930
+ setMeta(db, "embeddingFingerprint", currentFingerprint);
931
+ return { success: true };
932
+ }
933
+ finally {
934
+ if (heartbeatTimer)
935
+ clearInterval(heartbeatTimer);
875
936
  }
876
- onProgress({
877
- phase: "embeddings",
878
- message: `Stored ${storedCount} embedding${storedCount === 1 ? "" : "s"}.`,
879
- });
880
- setMeta(db, "embeddingFingerprint", currentFingerprint);
881
- return { success: true };
882
937
  }
883
938
  catch (error) {
884
939
  const message = error instanceof Error ? error.message : String(error);
@@ -0,0 +1,51 @@
1
+ // This Source Code Form is subject to the terms of the Mozilla Public
2
+ // License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+ /**
5
+ * Pure FTS5 query-string helpers, extracted from indexer/db/db.ts.
6
+ *
7
+ * These transform a raw user query into an FTS5-safe MATCH expression. They
8
+ * touch no database state, so they are unit-testable with zero DB setup.
9
+ */
10
+ /**
11
+ * Sanitize a raw user query into an FTS5-safe implicit-AND expression.
12
+ *
13
+ * Allows only characters safe in FTS5 queries: letters, digits, underscores,
14
+ * and whitespace. Everything else (hyphens, dots, quotes, parens, asterisks,
15
+ * colons, carets, @, !, etc.) is replaced with a space so that compound
16
+ * identifiers like "code-review" or "k8s.setup" become AND-joined tokens
17
+ * ("code review", "k8s setup") rather than triggering FTS5 syntax errors.
18
+ */
19
+ export function sanitizeFtsQuery(query) {
20
+ let sanitized = query.replace(/[^a-zA-Z0-9_\s]/g, " ");
21
+ // Neutralize the NEAR operator (FTS5 proximity syntax)
22
+ sanitized = sanitized.replace(/\bNEAR\b/g, " ");
23
+ const tokens = sanitized.split(/\s+/).filter((t) => t.length >= 1);
24
+ if (tokens.length === 0)
25
+ return "";
26
+ // Use implicit AND (space-separated tokens) for precision. FTS5 treats
27
+ // space-separated tokens as an implicit AND, matching only rows that
28
+ // contain ALL terms.
29
+ return tokens.join(" ");
30
+ }
31
+ /**
32
+ * Build a prefix query from an FTS5 query string by appending `*` to each
33
+ * token that is 3+ characters long. Tokens shorter than 3 characters are
34
+ * kept as-is (no prefix expansion) to avoid overly broad matches.
35
+ *
36
+ * Returns null if no tokens qualify for prefix expansion.
37
+ */
38
+ export function buildPrefixQuery(ftsQuery) {
39
+ const tokens = ftsQuery.split(/\s+/).filter(Boolean);
40
+ let hasPrefix = false;
41
+ const prefixTokens = tokens.map((t) => {
42
+ if (t.length >= 3) {
43
+ hasPrefix = true;
44
+ return `${t}*`;
45
+ }
46
+ return t;
47
+ });
48
+ if (!hasPrefix)
49
+ return null;
50
+ return prefixTokens.join(" ");
51
+ }
@@ -17,6 +17,7 @@
17
17
  import fs from "node:fs";
18
18
  import os from "node:os";
19
19
  import path from "node:path";
20
+ import { parseEmbeddedJsonResponse } from "../../core/parse.js";
20
21
  import { spawn as runtimeSpawn } from "../../runtime.js";
21
22
  import { getCommandBuilder } from "./builders.js";
22
23
  import { DEFAULT_AGENT_TIMEOUT_MS } from "./config.js";
@@ -346,72 +347,20 @@ export async function runAgent(profile, prompt, options = {}) {
346
347
  };
347
348
  }
348
349
  if (parseOutput === "json" && stdioMode === "captured") {
349
- // Strip <think> blocks and code fences, then try direct parse with
350
- // embedded-JSON fallback for local LLMs that emit prose around the payload.
351
- const cleaned = stdout
352
- .trim()
353
- .replace(/<think>[\s\S]*?<\/think>/gi, "")
354
- .trim()
355
- .replace(/^```(?:json)?\s*\n?/, "")
356
- .replace(/\n?```\s*$/, "")
357
- .trim();
358
- let parsed;
359
- try {
360
- parsed = JSON.parse(cleaned);
361
- }
362
- catch {
363
- // Fallback: extract the first balanced {…} from prose output.
364
- let found;
365
- for (let s = 0; s < cleaned.length; s++) {
366
- if (cleaned[s] !== "{")
367
- continue;
368
- let depth = 0, inStr = false, esc = false;
369
- for (let i = s; i < cleaned.length; i++) {
370
- const c = cleaned[i];
371
- if (inStr) {
372
- if (esc) {
373
- esc = false;
374
- }
375
- else if (c === "\\") {
376
- esc = true;
377
- }
378
- else if (c === '"') {
379
- inStr = false;
380
- }
381
- continue;
382
- }
383
- if (c === '"') {
384
- inStr = true;
385
- continue;
386
- }
387
- if (c === "{")
388
- depth++;
389
- if (c === "}") {
390
- depth--;
391
- if (depth === 0) {
392
- try {
393
- found = JSON.parse(cleaned.slice(s, i + 1));
394
- }
395
- catch { }
396
- break;
397
- }
398
- }
399
- }
400
- if (found !== undefined)
401
- break;
402
- }
403
- if (found === undefined) {
404
- return {
405
- ok: false,
406
- exitCode,
407
- stdout,
408
- stderr,
409
- durationMs,
410
- reason: "parse_error",
411
- error: "no JSON object found in agent output",
412
- };
413
- }
414
- parsed = found;
350
+ // Strip <think> blocks and code fences, then parse with embedded-JSON
351
+ // fallback for local LLMs that emit prose around the payload. Handles
352
+ // both top-level `{…}` and `[…]` structures.
353
+ const parsed = parseEmbeddedJsonResponse(stdout);
354
+ if (parsed === undefined) {
355
+ return {
356
+ ok: false,
357
+ exitCode,
358
+ stdout,
359
+ stderr,
360
+ durationMs,
361
+ reason: "parse_error",
362
+ error: "no JSON structure found in agent output",
363
+ };
415
364
  }
416
365
  return { ok: true, exitCode, stdout, stderr, durationMs, parsed };
417
366
  }
@@ -1058,6 +1058,19 @@ export function formatIndexPlain(r) {
1058
1058
  if (verification?.ok === false && verification.message) {
1059
1059
  out += `\nVerification: ${String(verification.message)}`;
1060
1060
  }
1061
+ const timing = indexResult.timing;
1062
+ if (timing) {
1063
+ out +=
1064
+ `\nTiming: total ${timing.totalMs}ms` +
1065
+ `, preflight ${timing.preflightMs}ms` +
1066
+ `, walk ${timing.walkMs}ms` +
1067
+ `, llm ${timing.llmMs}ms` +
1068
+ `, embeddings ${timing.embedMs}ms` +
1069
+ `, fts ${timing.ftsMs}ms` +
1070
+ `, finalize ${timing.finalizeMs}ms` +
1071
+ `, clean ${timing.cleanMs}ms` +
1072
+ `, end-to-end ${timing.endToEndMs}ms`;
1073
+ }
1061
1074
  return out;
1062
1075
  }
1063
1076
  export function formatListPlain(r) {