akm-cli 0.9.24 → 0.9.25-alpha.2

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 (84) hide show
  1. package/CHANGELOG.md +173 -0
  2. package/dist/cli.js +1 -1
  3. package/dist/commands/health/checks.js +10 -11
  4. package/dist/commands/improve/consolidate/pair-pass.js +4 -2
  5. package/dist/commands/improve/consolidate.js +10 -4
  6. package/dist/commands/improve/execution.js +4 -11
  7. package/dist/commands/improve/extract-prompt.js +4 -4
  8. package/dist/commands/improve/extract.js +11 -13
  9. package/dist/commands/improve/improve-cli.js +65 -34
  10. package/dist/commands/improve/improve-strategies.js +49 -43
  11. package/dist/commands/improve/improve-usage-report.js +8 -17
  12. package/dist/commands/improve/loop-stages.js +3 -0
  13. package/dist/commands/improve/preparation.js +3 -1
  14. package/dist/commands/improve/reflect-noise.js +125 -0
  15. package/dist/commands/improve/reflect.js +105 -172
  16. package/dist/commands/improve/retrieval-gate.js +7 -2
  17. package/dist/commands/improve/stage.js +67 -24
  18. package/dist/commands/proposal/drain.js +11 -2
  19. package/dist/commands/proposal/proposal-cli.js +1 -5
  20. package/dist/commands/proposal/propose-cli.js +2 -2
  21. package/dist/commands/proposal/propose.js +72 -84
  22. package/dist/commands/proposal/validators/proposal-quality-validators.js +4 -2
  23. package/dist/commands/read/search-cli.js +0 -38
  24. package/dist/commands/remember.js +3 -3
  25. package/dist/commands/sources/schema-repair.js +1 -1
  26. package/dist/core/config/config-schema.js +37 -60
  27. package/dist/core/config/engine-semantics.js +15 -11
  28. package/dist/core/config/schema/improve-processes.js +18 -2
  29. package/dist/core/improve-result.js +3 -3
  30. package/dist/core/redaction.js +4 -0
  31. package/dist/core/spawn-env.js +25 -0
  32. package/dist/core/structured.js +11 -1
  33. package/dist/execution/source.js +10 -0
  34. package/dist/indexer/passes/memory-inference.js +2 -1
  35. package/dist/integrations/agent/builder-shared.js +15 -0
  36. package/dist/integrations/agent/config.js +1 -1
  37. package/dist/integrations/agent/engine-resolution.js +13 -31
  38. package/dist/integrations/agent/execution.js +48 -22
  39. package/dist/integrations/agent/index.js +1 -1
  40. package/dist/integrations/agent/profiles.js +2 -2
  41. package/dist/integrations/agent/prompts.js +55 -114
  42. package/dist/integrations/agent/request-lowering.js +21 -8
  43. package/dist/integrations/agent/runner-dispatch.js +96 -3
  44. package/dist/integrations/agent/runner.js +8 -2
  45. package/dist/integrations/harnesses/aider/agent-builder.js +10 -23
  46. package/dist/integrations/harnesses/aider/index.js +0 -5
  47. package/dist/integrations/harnesses/amazonq/agent-builder.js +9 -21
  48. package/dist/integrations/harnesses/amazonq/index.js +0 -5
  49. package/dist/integrations/harnesses/claude/agent-builder.js +47 -36
  50. package/dist/integrations/harnesses/claude/index.js +0 -14
  51. package/dist/integrations/harnesses/codex/agent-builder.js +3 -4
  52. package/dist/integrations/harnesses/codex/index.js +0 -4
  53. package/dist/integrations/harnesses/copilot/agent-builder.js +4 -13
  54. package/dist/integrations/harnesses/copilot/index.js +2 -7
  55. package/dist/integrations/harnesses/gemini/agent-builder.js +4 -13
  56. package/dist/integrations/harnesses/gemini/index.js +0 -5
  57. package/dist/integrations/harnesses/ids.js +12 -10
  58. package/dist/integrations/harnesses/opencode/agent-builder.js +41 -9
  59. package/dist/integrations/harnesses/opencode/index.js +0 -8
  60. package/dist/integrations/harnesses/opencode/model-config.js +33 -0
  61. package/dist/integrations/harnesses/opencode/model-work-agent.js +115 -0
  62. package/dist/integrations/harnesses/opencode-sdk/harness.js +2 -7
  63. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +114 -28
  64. package/dist/integrations/harnesses/openhands/agent-builder.js +7 -21
  65. package/dist/integrations/harnesses/openhands/index.js +0 -5
  66. package/dist/integrations/harnesses/pi/agent-builder.js +7 -21
  67. package/dist/integrations/harnesses/pi/index.js +0 -5
  68. package/dist/llm/client.js +5 -0
  69. package/dist/llm/feature-gate.js +12 -9
  70. package/dist/llm/index-passes.js +2 -5
  71. package/dist/llm/memory-infer.js +6 -5
  72. package/dist/llm/structured-call.js +33 -11
  73. package/dist/output/shapes/passthrough.js +1 -0
  74. package/dist/scripts/akm-migrate-node.js +298 -228
  75. package/dist/scripts/akm-migrate.js +298 -228
  76. package/dist/workflows/exec/step-work.js +6 -5
  77. package/dist/workflows/exec/unit-dispatch.js +4 -13
  78. package/dist/workflows/freeze/step-values.js +1 -1
  79. package/docs/reference/cli.md +41 -18
  80. package/docs/reference/configuration.md +165 -12
  81. package/docs/reference/data-and-telemetry.md +2 -3
  82. package/docs/reference/workflow-schema.md +10 -9
  83. package/package.json +1 -1
  84. package/schemas/akm-config.json +108 -0
@@ -11,11 +11,12 @@ import thorough from "../../assets/improve-strategies/thorough.json" with { type
11
11
  import { conceptIdFromTypeName, parseRefInput } from "../../core/asset/resolve-ref.js";
12
12
  import { ImproveProfileConfigSchema } from "../../core/config/config-schema.js";
13
13
  import { deepMergeConfig } from "../../core/config/deep-merge.js";
14
- import { BUILTIN_IMPROVE_STRATEGY_NAMES, IMPROVE_PROCESS_ENGINE_CAPABILITIES, } from "../../core/config/engine-semantics.js";
14
+ import { BUILTIN_IMPROVE_STRATEGY_NAMES, IMPROVE_ENGINE_PROCESSES, IMPROVE_PROCESS_NAMES, } from "../../core/config/engine-semantics.js";
15
15
  import { ConfigError } from "../../core/errors.js";
16
- import { describeLlmCredentialAvailability } from "../../integrations/agent/engine-resolution.js";
16
+ import { describeLlmCredentialAvailability, } from "../../integrations/agent/engine-resolution.js";
17
+ import { runnerLlmConnection } from "../../integrations/agent/runner.js";
17
18
  import { applyAutonomyGate } from "./autonomy-gate.js";
18
- import { resolveImproveExecution, resolveImproveLlmExecution } from "./execution.js";
19
+ import { resolveImproveExecution } from "./execution.js";
19
20
  import { stripBundle } from "./ledger.js";
20
21
  export const DEFAULT_ALLOWED_TYPES = {
21
22
  reflect: ["agent", "command", "knowledge", "lesson", "memory", "skill", "workflow"],
@@ -113,9 +114,14 @@ export function resolveImproveStrategy(name, config) {
113
114
  const resolved = deepMergeConfig(baseStrategy, (userStrategies[selectedName] ?? {}));
114
115
  return { name: selectedName, config: ImproveProfileConfigSchema.parse(resolved) };
115
116
  }
117
+ /**
118
+ * The processes that make their own model calls, so the plan resolves a runner
119
+ * for each: every engine process but triage, whose engine is its judgment's.
120
+ */
121
+ export const MODEL_CALLING_PROCESSES = new Set(IMPROVE_ENGINE_PROCESSES.filter((name) => name !== "triage"));
116
122
  /**
117
123
  * Project a resolved improve plan into one routing row per
118
- * {@link IMPROVE_PROCESS_ENGINE_CAPABILITIES} name, plus a `"triage.judgment"`
124
+ * {@link IMPROVE_PROCESS_NAMES} name, plus a `"triage.judgment"`
119
125
  * pseudo-row when the strategy configures a judgment engine. Pure — no I/O,
120
126
  * no re-resolution. Shared by `improve --dry-run`'s `plan.processes` (#947)
121
127
  * and `akm health`'s post-run reporting (#944); health's own
@@ -124,14 +130,18 @@ export function resolveImproveStrategy(name, config) {
124
130
  export function projectResolvedProcessRouting(plan) {
125
131
  const unavailableByProcess = new Map(plan.engineUnavailable.map((item) => [item.process, item]));
126
132
  const rows = [];
127
- for (const processName of Object.keys(IMPROVE_PROCESS_ENGINE_CAPABILITIES)) {
133
+ for (const processName of IMPROVE_PROCESS_NAMES) {
128
134
  const process = plan.processes[processName];
129
135
  const unavailable = unavailableByProcess.get(processName);
130
136
  rows.push({
131
137
  process: processName,
132
138
  enabled: process.enabled,
133
139
  ...(process.runner
134
- ? { engine: process.runner.engine, model: process.runner.connection.model, engineKind: process.runner.kind }
140
+ ? {
141
+ engine: process.runner.engine,
142
+ model: runnerLlmConnection(process.runner)?.model,
143
+ engineKind: process.runner.kind,
144
+ }
135
145
  : // #800/#957 round 3 — a credential-unavailable process never carries a
136
146
  // runner, but its structurally resolved engine/model is still worth
137
147
  // showing in the routing table (dry-run preview, health probe).
@@ -192,6 +202,22 @@ export function resolveImprovePlan(name, config, options = {}) {
192
202
  function credentialUnavailableReason(engineName, status) {
193
203
  return `requires a credential that is not available in this process's environment (engine "${engineName}": ${status.reason})`;
194
204
  }
205
+ /**
206
+ * Whether the credential akm reads for `runner` is reachable in `env`: an LLM
207
+ * engine's own, or an SDK engine's LLM fallback. An agent CLI reads its own.
208
+ */
209
+ function runnerCredentialStatus(runner, env) {
210
+ if (runner.kind === "llm")
211
+ return describeLlmCredentialAvailability(runner, env);
212
+ if (runner.kind === "sdk") {
213
+ return describeLlmCredentialAvailability({
214
+ credential: runner.fallbackCredential,
215
+ apiKeyFile: runner.fallbackApiKeyFile,
216
+ apiKeySecretRef: runner.fallbackApiKeySecretRef,
217
+ }, env);
218
+ }
219
+ return { available: true };
220
+ }
195
221
  function buildImprovePlan(strategy, config, options) {
196
222
  const env = options.env ?? process.env;
197
223
  const processes = {};
@@ -205,12 +231,12 @@ function buildImprovePlan(strategy, config, options) {
205
231
  // install is not the credential-in-the-wrong-environment case this option
206
232
  // exists for.
207
233
  let anyEngineNotConfigured = false;
208
- for (const processName of Object.keys(IMPROVE_PROCESS_ENGINE_CAPABILITIES)) {
234
+ for (const processName of IMPROVE_PROCESS_NAMES) {
209
235
  const sourceProcessConfig = strategy.config.processes?.[processName] ?? {};
210
236
  const enabled = sourceProcessConfig.enabled === true;
211
237
  let runner = null;
212
238
  let notices = [];
213
- if (IMPROVE_PROCESS_ENGINE_CAPABILITIES[processName] !== "llm" || !enabled) {
239
+ if (!MODEL_CALLING_PROCESSES.has(processName) || !enabled) {
214
240
  processes[processName] = Object.freeze({ enabled, config: cloneAndFreeze(sourceProcessConfig), runner });
215
241
  continue;
216
242
  }
@@ -227,7 +253,7 @@ function buildImprovePlan(strategy, config, options) {
227
253
  // without the runner ever carrying a credential-less connection forward.
228
254
  let credentialUnavailableRouting;
229
255
  if (!skipsRepairEngine) {
230
- const resolved = resolveImproveLlmExecution({
256
+ const resolved = resolveImproveExecution({
231
257
  config,
232
258
  profile: strategy.config,
233
259
  process: sourceProcessConfig,
@@ -236,15 +262,14 @@ function buildImprovePlan(strategy, config, options) {
236
262
  runner = resolved?.runner ?? null;
237
263
  notices = resolved?.notices ?? [];
238
264
  if (runner) {
239
- const credentialStatus = describeLlmCredentialAvailability(runner, env);
265
+ const credentialStatus = runnerCredentialStatus(runner, env);
240
266
  if (!credentialStatus.available) {
267
+ const connection = runnerLlmConnection(runner);
241
268
  credentialUnavailableMessage = credentialUnavailableReason(runner.engine, credentialStatus);
242
269
  credentialUnavailableRouting = {
243
270
  engine: runner.engine,
244
- ...(runner.connection.model !== undefined ? { model: runner.connection.model } : {}),
245
- ...(runner.connection.contextLength !== undefined
246
- ? { contextLength: runner.connection.contextLength }
247
- : {}),
271
+ ...(connection?.model !== undefined ? { model: connection.model } : {}),
272
+ ...(connection?.contextLength !== undefined ? { contextLength: connection.contextLength } : {}),
248
273
  };
249
274
  runner = null;
250
275
  notices = [];
@@ -259,7 +284,7 @@ function buildImprovePlan(strategy, config, options) {
259
284
  process: processName,
260
285
  configKey,
261
286
  reason: credentialUnavailableMessage ??
262
- `requires an LLM engine that is not configured. Set defaults.llmEngine or ${configKey}`,
287
+ `requires an engine that is not configured. Set defaults.llmEngine or ${configKey}`,
263
288
  ...credentialUnavailableRouting,
264
289
  });
265
290
  processes[processName] = Object.freeze({
@@ -282,7 +307,7 @@ function buildImprovePlan(strategy, config, options) {
282
307
  !Object.values(processes).some((process) => process.enabled) &&
283
308
  (!options.allowAllDisabled || anyEngineNotConfigured)) {
284
309
  const names = engineUnavailable.map((item) => `"${item.process}"`).join(", ");
285
- throw new ConfigError(`No improve process can run: ${names} ${engineUnavailable.length === 1 ? "requires" : "require"} an LLM engine that is not configured. Set defaults.llmEngine, or the per-process engine key named for each.`, "LLM_NOT_CONFIGURED");
310
+ throw new ConfigError(`No improve process can run: ${names} ${engineUnavailable.length === 1 ? "requires" : "require"} an engine that is not configured. Set defaults.llmEngine, or the per-process engine key named for each.`, "LLM_NOT_CONFIGURED");
286
311
  }
287
312
  const triage = strategy.config.processes?.triage;
288
313
  const judgmentEnabled = triage?.judgment?.enabled === true;
@@ -296,10 +321,6 @@ function buildImprovePlan(strategy, config, options) {
296
321
  })
297
322
  : null;
298
323
  let triageJudgment = triageJudgmentResolution?.runner ?? null;
299
- const effectiveJudgmentLlm = triage?.judgment?.llm ?? triage?.llm ?? strategy.config.llm;
300
- if (triageJudgment && triageJudgment.kind !== "llm" && effectiveJudgmentLlm) {
301
- throw new ConfigError(`Triage judgment engine "${triageJudgment.engine ?? "unknown"}" is an agent engine and cannot receive llm overrides.`, "INVALID_CONFIG_FILE");
302
- }
303
324
  if (processes.triage.enabled && judgmentEnabled && !triageJudgment) {
304
325
  throw new ConfigError(`Enabled improve triage judgment requires an engine. Set defaults.llmEngine or improve.strategies.${strategy.name}.processes.triage.judgment.engine.`, "LLM_NOT_CONFIGURED");
305
326
  }
@@ -310,29 +331,14 @@ function buildImprovePlan(strategy, config, options) {
310
331
  // `engineUnavailable` under the reserved `"triage.judgment"` process name
311
332
  // instead of silently reaching dispatch with a doomed credential.
312
333
  if (triageJudgment) {
313
- const judgmentCredentialFields = triageJudgment.kind === "llm"
314
- ? {
315
- credential: triageJudgment.credential,
316
- apiKeyFile: triageJudgment.apiKeyFile,
317
- apiKeySecretRef: triageJudgment.apiKeySecretRef,
318
- }
319
- : triageJudgment.kind === "sdk"
320
- ? {
321
- credential: triageJudgment.fallbackCredential,
322
- apiKeyFile: triageJudgment.fallbackApiKeyFile,
323
- apiKeySecretRef: triageJudgment.fallbackApiKeySecretRef,
324
- }
325
- : undefined;
326
- if (judgmentCredentialFields) {
327
- const credentialStatus = describeLlmCredentialAvailability(judgmentCredentialFields, env);
328
- if (!credentialStatus.available) {
329
- engineUnavailable.push({
330
- process: "triage.judgment",
331
- configKey: `improve.strategies.${strategy.name}.processes.triage.judgment.engine`,
332
- reason: credentialUnavailableReason(triageJudgment.engine, credentialStatus),
333
- });
334
- triageJudgment = null;
335
- }
334
+ const credentialStatus = runnerCredentialStatus(triageJudgment, env);
335
+ if (!credentialStatus.available) {
336
+ engineUnavailable.push({
337
+ process: "triage.judgment",
338
+ configKey: `improve.strategies.${strategy.name}.processes.triage.judgment.engine`,
339
+ reason: credentialUnavailableReason(triageJudgment.engine, credentialStatus),
340
+ });
341
+ triageJudgment = null;
336
342
  }
337
343
  }
338
344
  const frozenProcesses = Object.freeze(processes);
@@ -1,20 +1,8 @@
1
1
  // This Source Code Form is subject to the terms of the Mozilla Public
2
2
  // License, v. 2.0. If a copy of the MPL was not distributed with this
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
- /**
5
- * The per-run LLM usage report (#944): the process × engine × model cross-tab
6
- * plus the enabled processes that made no call and why, rendered as one table
7
- * for the end-of-run stderr summary and `akm improve report`.
8
- */
9
- import { IMPROVE_PROCESS_ENGINE_CAPABILITIES } from "../../core/config/engine-semantics.js";
10
- import { eligibleRefCount, projectResolvedProcessRouting, } from "./improve-strategies.js";
11
- /**
12
- * Only processes that call an LLM themselves: triage (a runner, attributed to
13
- * its judgment engine) and proactive maintenance (no engine) would always read
14
- * as "zero calls".
15
- */
16
- const LLM_BACKED_PROCESSES = new Set(Object.keys(IMPROVE_PROCESS_ENGINE_CAPABILITIES).filter((name) => IMPROVE_PROCESS_ENGINE_CAPABILITIES[name] === "llm"));
17
- /** The autonomy lane gating each LLM-backed process, if any. */
4
+ import { eligibleRefCount, MODEL_CALLING_PROCESSES, projectResolvedProcessRouting, } from "./improve-strategies.js";
5
+ /** The autonomy lane gating each model-calling process, if any. */
18
6
  const AUTONOMY_LANE_BY_PROCESS = {
19
7
  memoryInference: "memoryInference",
20
8
  };
@@ -30,7 +18,7 @@ function dominantReason(counts) {
30
18
  return best;
31
19
  }
32
20
  /**
33
- * Why an LLM-backed process made no call, in priority order: its engine is
21
+ * Why a model-calling process made no call, in priority order: its engine is
34
22
  * unavailable; disabled — `autonomy_gated` when its lane was gated, else
35
23
  * nothing to report; every ref strategy-filtered; its dominant skip reason;
36
24
  * else `no_signal`. The reasons are the ones the events and results already
@@ -65,8 +53,11 @@ function countReflectSkipReasons(actions) {
65
53
  }
66
54
  /** This run's `usageReport`, or `undefined` when both halves are empty. */
67
55
  export function buildImproveUsageReport(args) {
68
- // The "triage.judgment" row is dropped too: its calls are never attributed to it (#947).
69
- const routing = projectResolvedProcessRouting(args.resolvedPlan).filter((row) => row.process !== "triage.judgment" && LLM_BACKED_PROCESSES.has(row.process));
56
+ // Only the processes that call a model themselves, on any engine kind: triage
57
+ // (attributed to its judgment) and proactive maintenance (no engine) would
58
+ // always read as "zero calls". The "triage.judgment" row is dropped too: its
59
+ // calls are never attributed to it (#947).
60
+ const routing = projectResolvedProcessRouting(args.resolvedPlan).filter((row) => row.process !== "triage.judgment" && MODEL_CALLING_PROCESSES.has(row.process));
70
61
  const calledProcesses = new Set(args.byProcessEngineModel.filter((row) => row.calls > 0).map((row) => row.process));
71
62
  const reflectSkipCounts = countReflectSkipReasons(args.persistedActions);
72
63
  const noCalls = [];
@@ -149,6 +149,9 @@ async function runLoopReflectPass(planned, env, tally) {
149
149
  ...(reflectErrors.length > 0 ? { avoidPatterns: [...reflectErrors] } : {}),
150
150
  eventSource: "improve",
151
151
  lowValueFilter: improveProfile.processes?.reflect?.lowValueFilter?.enabled === true,
152
+ ...(improveProfile.processes?.reflect?.defectFilter
153
+ ? { defectFilter: improveProfile.processes.reflect.defectFilter }
154
+ : {}),
152
155
  ...(budgetMs > 0 ? { timeoutMs: budgetMs } : {}),
153
156
  signal: env.budgetSignal,
154
157
  eventsCtx: env.eventsCtx,
@@ -24,6 +24,7 @@ import { appendEvent, readEvents } from "../../core/events.js";
24
24
  import { withStateDb } from "../../core/state-db.js";
25
25
  import { info, warn } from "../../core/warn.js";
26
26
  import { countUsageEventsByType, USAGE_EVENT_RETENTION_DAYS } from "../../indexer/usage/usage-events.js";
27
+ import { runnerLlmConnection } from "../../integrations/agent/runner.js";
27
28
  import { getAvailableHarnesses } from "../../integrations/session-logs/index.js";
28
29
  import { getZeroResultSearches } from "../../storage/repositories/index-entries-repository.js";
29
30
  import { getRetrievalCounts } from "../../storage/repositories/index-utility-repository.js";
@@ -119,7 +120,8 @@ function planConsolidationPass(args) {
119
120
  : { poolSize: 0, candidatePoolSize: 0, judgedUnchanged: 0 };
120
121
  // A credential-unavailable engine still resolved its context length.
121
122
  const unavailable = resolvedPlan.engineUnavailable.find((item) => item.process === "consolidate");
122
- const chunkSize = computeSafeChunkSize(resolvedPlan.processes.consolidate.runner?.connection.contextLength ??
123
+ const chunkSize = computeSafeChunkSize((resolvedPlan.processes.consolidate.runner &&
124
+ runnerLlmConnection(resolvedPlan.processes.consolidate.runner)?.contextLength) ??
123
125
  unavailable?.contextLength ??
124
126
  DEFAULT_CONTEXT_LENGTH_TOKENS, 500, processConfig?.maxChunkSize);
125
127
  const profilePassed = processConfig?.enabled !== false;
@@ -9,8 +9,13 @@
9
9
  * cosmetic edit through costs a review, suppressing a real fix loses work — so
10
10
  * anything uncertain is `substantive`: code (fenced or indented) compares
11
11
  * verbatim, and headings, tables and breaks never absorb the next line.
12
+ *
13
+ * `findReflectDefect` is its counterpart for an edit that is not noise but a
14
+ * defect no judge needs to weigh: placeholder text, talk about the edit itself,
15
+ * or frontmatter copied into the body.
12
16
  */
13
17
  import { parse as yamlParse } from "yaml";
18
+ import { parseFrontmatter } from "../../core/asset/frontmatter.js";
14
19
  /**
15
20
  * `noop` (identical up to trailing whitespace) and `cosmetic` (identical after
16
21
  * normalizing frontmatter as parsed YAML and prose as unwrapped text) never
@@ -36,6 +41,126 @@ export function classifyReflectChange(sourceContent, candidateContent) {
36
41
  }
37
42
  return "substantive";
38
43
  }
44
+ /** Not TODO, TBD or FIXME: an asset may carry those on purpose, and the owner does not want them refused. */
45
+ const DEFAULT_PLACEHOLDERS = [
46
+ "please confirm",
47
+ "please verify",
48
+ "to be confirmed",
49
+ "to be determined",
50
+ "to be verified",
51
+ ];
52
+ const DEFAULT_META_COMMENTARY = [
53
+ "feedback signal",
54
+ "feedback signals",
55
+ "feedback indicate",
56
+ "feedback indicates",
57
+ "feedback suggest",
58
+ "feedback suggests",
59
+ "feedback ask",
60
+ "feedback asks",
61
+ "feedback says",
62
+ "feedback report",
63
+ "feedback reports",
64
+ "feedback request",
65
+ "feedback requests",
66
+ "this revision",
67
+ "the source asset",
68
+ "the source note",
69
+ "the source memory",
70
+ "the original asset",
71
+ "the original note",
72
+ "the original memory",
73
+ "the original version of this",
74
+ "quality gate rejected",
75
+ "proposal rejected",
76
+ ];
77
+ const DEFAULT_FRONTMATTER_KEYS = [
78
+ "sources",
79
+ "updated",
80
+ "inferenceProcessed",
81
+ "captureMode",
82
+ "beliefState",
83
+ "xrefs",
84
+ "contradictedBy",
85
+ "outcomeData",
86
+ "orderedActions",
87
+ "generated",
88
+ "verified",
89
+ "description",
90
+ "when_to_use",
91
+ "tags",
92
+ "searchHints",
93
+ "quality",
94
+ "salience",
95
+ "salienceInputs",
96
+ "lint_skip",
97
+ "type",
98
+ ];
99
+ /**
100
+ * The first defect the candidate has and its source lacks, or `undefined`. Each
101
+ * rule counts only what the revision adds, so text the asset already carried is
102
+ * not held against it. On 396 labelled reflect edits (83 good, 313 bad) the
103
+ * default lists hit 22 bad edits and no good one, so a hit is refused unjudged.
104
+ */
105
+ export function findReflectDefect(sourceContent, candidateContent, filter = {}) {
106
+ if (gainsPhrases(filter.placeholders ?? DEFAULT_PLACEHOLDERS, sourceContent, candidateContent)) {
107
+ return "placeholder_added";
108
+ }
109
+ if (gainsPhrases(filter.metaCommentary ?? DEFAULT_META_COMMENTARY, sourceContent, candidateContent)) {
110
+ return "meta_commentary_added";
111
+ }
112
+ if (frontmatterCopiedIntoBody(sourceContent, candidateContent, filter.frontmatterKeys ?? DEFAULT_FRONTMATTER_KEYS)) {
113
+ return "frontmatter_copied_into_body";
114
+ }
115
+ return undefined;
116
+ }
117
+ /** Frontmatter fields that name other assets; one of their values newly in the body is provenance copied over. */
118
+ const PROVENANCE_KEYS = ["sources", "xrefs", "contradictedBy"];
119
+ /** Shorter values (a bare name or id) say too little to find in a body. */
120
+ const PROVENANCE_VALUE_MIN_CHARS = 12;
121
+ function escapeRegExp(text) {
122
+ return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
123
+ }
124
+ /** Whether the candidate holds more of the phrases than the source: whole words, any case, any whitespace between words. */
125
+ function gainsPhrases(phrases, source, candidate) {
126
+ const alternatives = phrases
127
+ .map((phrase) => phrase.trim().split(/\s+/).map(escapeRegExp).join("\\s+"))
128
+ .filter((alternative) => alternative !== "");
129
+ if (alternatives.length === 0)
130
+ return false;
131
+ const pattern = new RegExp(`(?<!\\w)(?:${alternatives.join("|")})(?!\\w)`, "gi");
132
+ return (candidate.match(pattern)?.length ?? 0) > (source.match(pattern)?.length ?? 0);
133
+ }
134
+ /** The body gains a line that starts with one of the frontmatter keys outside code, or a provenance value from either asset's frontmatter. */
135
+ function frontmatterCopiedIntoBody(source, candidate, keys) {
136
+ if (keys.length === 0)
137
+ return false;
138
+ const keyLine = new RegExp(`^(?:${keys.map(escapeRegExp).join("|")}):(?:[ \\t].*)?$`);
139
+ const keyLines = (text) => parseLowValueSections(text).proseLines.filter((line) => keyLine.test(line)).length;
140
+ if (keyLines(candidate) > keyLines(source))
141
+ return true;
142
+ const sourceBody = lettersAndDigits(splitFrontmatter(source).body);
143
+ const candidateBody = lettersAndDigits(splitFrontmatter(candidate).body);
144
+ return [...provenanceValues(source), ...provenanceValues(candidate)].some((value) => {
145
+ const v = lettersAndDigits(value);
146
+ return v.length >= PROVENANCE_VALUE_MIN_CHARS && candidateBody.includes(v) && !sourceBody.includes(v);
147
+ });
148
+ }
149
+ /** Every string under the provenance keys of a frontmatter block: a scalar, or the items of a list. */
150
+ function provenanceValues(content) {
151
+ const { data } = parseFrontmatter(content);
152
+ return PROVENANCE_KEYS.flatMap((key) => {
153
+ const value = data[key];
154
+ return (Array.isArray(value) ? value : [value]).filter((item) => typeof item === "string");
155
+ });
156
+ }
157
+ /** Lower-case letters and digits, each run of anything else a single space. */
158
+ function lettersAndDigits(text) {
159
+ return text
160
+ .toLowerCase()
161
+ .replace(/[^a-z0-9]+/g, " ")
162
+ .trim();
163
+ }
39
164
  /** A `---` frontmatter block and the rest (`fmText: null` when there is none). */
40
165
  export function splitFrontmatter(raw) {
41
166
  const m = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);