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.
- package/CHANGELOG.md +173 -0
- package/dist/cli.js +1 -1
- package/dist/commands/health/checks.js +10 -11
- package/dist/commands/improve/consolidate/pair-pass.js +4 -2
- package/dist/commands/improve/consolidate.js +10 -4
- package/dist/commands/improve/execution.js +4 -11
- package/dist/commands/improve/extract-prompt.js +4 -4
- package/dist/commands/improve/extract.js +11 -13
- package/dist/commands/improve/improve-cli.js +65 -34
- package/dist/commands/improve/improve-strategies.js +49 -43
- package/dist/commands/improve/improve-usage-report.js +8 -17
- package/dist/commands/improve/loop-stages.js +3 -0
- package/dist/commands/improve/preparation.js +3 -1
- package/dist/commands/improve/reflect-noise.js +125 -0
- package/dist/commands/improve/reflect.js +105 -172
- package/dist/commands/improve/retrieval-gate.js +7 -2
- package/dist/commands/improve/stage.js +67 -24
- package/dist/commands/proposal/drain.js +11 -2
- package/dist/commands/proposal/proposal-cli.js +1 -5
- package/dist/commands/proposal/propose-cli.js +2 -2
- package/dist/commands/proposal/propose.js +72 -84
- package/dist/commands/proposal/validators/proposal-quality-validators.js +4 -2
- package/dist/commands/read/search-cli.js +0 -38
- package/dist/commands/remember.js +3 -3
- package/dist/commands/sources/schema-repair.js +1 -1
- package/dist/core/config/config-schema.js +37 -60
- package/dist/core/config/engine-semantics.js +15 -11
- package/dist/core/config/schema/improve-processes.js +18 -2
- package/dist/core/improve-result.js +3 -3
- package/dist/core/redaction.js +4 -0
- package/dist/core/spawn-env.js +25 -0
- package/dist/core/structured.js +11 -1
- package/dist/execution/source.js +10 -0
- package/dist/indexer/passes/memory-inference.js +2 -1
- package/dist/integrations/agent/builder-shared.js +15 -0
- package/dist/integrations/agent/config.js +1 -1
- package/dist/integrations/agent/engine-resolution.js +13 -31
- package/dist/integrations/agent/execution.js +48 -22
- package/dist/integrations/agent/index.js +1 -1
- package/dist/integrations/agent/profiles.js +2 -2
- package/dist/integrations/agent/prompts.js +55 -114
- package/dist/integrations/agent/request-lowering.js +21 -8
- package/dist/integrations/agent/runner-dispatch.js +96 -3
- package/dist/integrations/agent/runner.js +8 -2
- package/dist/integrations/harnesses/aider/agent-builder.js +10 -23
- package/dist/integrations/harnesses/aider/index.js +0 -5
- package/dist/integrations/harnesses/amazonq/agent-builder.js +9 -21
- package/dist/integrations/harnesses/amazonq/index.js +0 -5
- package/dist/integrations/harnesses/claude/agent-builder.js +47 -36
- package/dist/integrations/harnesses/claude/index.js +0 -14
- package/dist/integrations/harnesses/codex/agent-builder.js +3 -4
- package/dist/integrations/harnesses/codex/index.js +0 -4
- package/dist/integrations/harnesses/copilot/agent-builder.js +4 -13
- package/dist/integrations/harnesses/copilot/index.js +2 -7
- package/dist/integrations/harnesses/gemini/agent-builder.js +4 -13
- package/dist/integrations/harnesses/gemini/index.js +0 -5
- package/dist/integrations/harnesses/ids.js +12 -10
- package/dist/integrations/harnesses/opencode/agent-builder.js +41 -9
- package/dist/integrations/harnesses/opencode/index.js +0 -8
- package/dist/integrations/harnesses/opencode/model-config.js +33 -0
- package/dist/integrations/harnesses/opencode/model-work-agent.js +115 -0
- package/dist/integrations/harnesses/opencode-sdk/harness.js +2 -7
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +114 -28
- package/dist/integrations/harnesses/openhands/agent-builder.js +7 -21
- package/dist/integrations/harnesses/openhands/index.js +0 -5
- package/dist/integrations/harnesses/pi/agent-builder.js +7 -21
- package/dist/integrations/harnesses/pi/index.js +0 -5
- package/dist/llm/client.js +5 -0
- package/dist/llm/feature-gate.js +12 -9
- package/dist/llm/index-passes.js +2 -5
- package/dist/llm/memory-infer.js +6 -5
- package/dist/llm/structured-call.js +33 -11
- package/dist/output/shapes/passthrough.js +1 -0
- package/dist/scripts/akm-migrate-node.js +298 -228
- package/dist/scripts/akm-migrate.js +298 -228
- package/dist/workflows/exec/step-work.js +6 -5
- package/dist/workflows/exec/unit-dispatch.js +4 -13
- package/dist/workflows/freeze/step-values.js +1 -1
- package/docs/reference/cli.md +41 -18
- package/docs/reference/configuration.md +165 -12
- package/docs/reference/data-and-telemetry.md +2 -3
- package/docs/reference/workflow-schema.md +10 -9
- package/package.json +1 -1
- 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,
|
|
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
|
|
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
|
|
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
|
|
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
|
-
? {
|
|
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
|
|
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 (
|
|
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 =
|
|
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 =
|
|
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
|
-
...(
|
|
245
|
-
...(
|
|
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
|
|
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
|
|
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
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
69
|
-
|
|
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
|
|
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]*)$/);
|