akm-cli 0.9.4 → 0.9.5
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 +160 -0
- package/dist/commands/improve/anti-collapse.js +4 -91
- package/dist/commands/proposal/validators/proposal-validators.js +12 -0
- package/dist/commands/read/search.js +14 -24
- package/dist/commands/tasks/tasks-cli.js +56 -3
- package/dist/commands/tasks/tasks.js +89 -2
- package/dist/core/adapter/adapters/akm-adapter.js +2 -0
- package/dist/core/config/config-version-shim.js +101 -0
- package/dist/core/config/config.js +6 -6
- package/dist/core/improve-result.js +35 -14
- package/dist/execution/guarded-source.js +0 -10
- package/dist/indexer/passes/metadata.js +12 -4
- package/dist/indexer/scan/doc-to-entry.js +2 -0
- package/dist/indexer/search/search-fields.js +16 -1
- package/dist/output/shapes/passthrough.js +17 -5
- package/dist/scripts/akm-migrate-node.js +120 -112
- package/dist/scripts/akm-migrate.js +120 -112
- package/dist/storage/repositories/proposals-repository.js +32 -6
- package/dist/tasks/scheduler-binding.js +15 -5
- package/dist/tasks/scheduler-sync-preview.js +4 -2
- package/dist/tasks/scheduler-sync.js +77 -42
- package/dist/tasks/source/task-to-v3.js +24 -12
- package/dist/tasks/source/task-to-v4.js +0 -10
- package/docs/reference/cli.md +25 -3
- package/docs/reference/configuration.md +27 -6
- package/docs/reference/tasks.md +10 -0
- package/package.json +1 -1
|
@@ -0,0 +1,101 @@
|
|
|
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
|
+
* The `configVersion` read shim (#863).
|
|
6
|
+
*
|
|
7
|
+
* `configVersion` used to be a hard `z.literal(CURRENT_CONFIG_VERSION)` gate:
|
|
8
|
+
* every value other than the exact current string threw
|
|
9
|
+
* `UNSUPPORTED_CONFIG_VERSION` for every command, since every akm invocation
|
|
10
|
+
* loads config first. That is the same shape of break #858/#859 (proposal
|
|
11
|
+
* rows), the `task_history` `metadataVersion` gate, and the task-source v2/v3
|
|
12
|
+
* gate each caused in 0.9.x — a version gate with no read shim — except
|
|
13
|
+
* `configVersion`'s blast radius is the whole CLI rather than one subsystem.
|
|
14
|
+
*
|
|
15
|
+
* This module mirrors the in-tree template for that fix,
|
|
16
|
+
* `src/tasks/source/parse-task-source.ts`'s v2/v3 -> v4 shim: a known-old
|
|
17
|
+
* version routes through a pure, in-memory upgrade function to the current
|
|
18
|
+
* shape, with a one-line stderr deprecation warning; the result is never
|
|
19
|
+
* written back to disk (the on-disk rewrite already happens for free — every
|
|
20
|
+
* `saveConfig`/`mutateConfig` write forces `configVersion` to
|
|
21
|
+
* {@link CURRENT_CONFIG_VERSION}, so the very next `akm config set` or any
|
|
22
|
+
* other mutating command silences the warning permanently). A version that is
|
|
23
|
+
* neither current nor a known old version — including anything NEWER than
|
|
24
|
+
* current — still fails closed with `UNSUPPORTED_CONFIG_VERSION`:
|
|
25
|
+
* forward-incompatibility is a real hazard (an older binary must not guess at
|
|
26
|
+
* a newer, unknown shape) and this shim does not soften that.
|
|
27
|
+
*
|
|
28
|
+
* IMPORTANT — as of this writing, `"0.9.0"` is the only `configVersion` akm
|
|
29
|
+
* has ever shipped; there is no real prior release to shim. `"0.0.1"` below
|
|
30
|
+
* is a SYNTHETIC placeholder entry that exists solely to stand up and
|
|
31
|
+
* exercise this mechanism — the known-versions list, the dispatch table, the
|
|
32
|
+
* warn-once-and-upgrade behavior, the fail-closed behavior for anything
|
|
33
|
+
* else — before a real bump ever needs it (see
|
|
34
|
+
* `tests/integration/config-version-shim.test.ts` and the
|
|
35
|
+
* `previous-release-corpus.test.ts` fixture). When the first genuine
|
|
36
|
+
* `configVersion` bump ships, add its real old shape as its own entry the
|
|
37
|
+
* same way and delete the synthetic `"0.0.1"` entry (and this paragraph) in
|
|
38
|
+
* the same change.
|
|
39
|
+
*/
|
|
40
|
+
import { ConfigError } from "../errors.js";
|
|
41
|
+
import { warn } from "../warn.js";
|
|
42
|
+
import { CURRENT_CONFIG_VERSION } from "./schema/primitives.js";
|
|
43
|
+
/**
|
|
44
|
+
* Every `configVersion` this binary can still READ, other than
|
|
45
|
+
* {@link CURRENT_CONFIG_VERSION} itself — each with an in-memory upgrade
|
|
46
|
+
* function in {@link CONFIG_VERSION_UPGRADES}. Anything not in this list (and
|
|
47
|
+
* not equal to current) fails closed.
|
|
48
|
+
*/
|
|
49
|
+
export const KNOWN_OLD_CONFIG_VERSIONS = ["0.0.1"];
|
|
50
|
+
function isKnownOldConfigVersion(value) {
|
|
51
|
+
return typeof value === "string" && KNOWN_OLD_CONFIG_VERSIONS.includes(value);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* SYNTHETIC 0.0.1 -> 0.9.0 upgrade (placeholder — see module doc). Per this
|
|
55
|
+
* placeholder, 0.0.1 kept the default LLM engine name at the config root as
|
|
56
|
+
* `defaultEngine`; 0.9.0 moved it under `defaults.llmEngine`. Pure function:
|
|
57
|
+
* takes the raw parsed JSON object, returns a new raw object with the 0.9.0
|
|
58
|
+
* shape. Never touches disk.
|
|
59
|
+
*/
|
|
60
|
+
function upgradeFrom080(raw) {
|
|
61
|
+
const { defaultEngine, defaults, ...rest } = raw;
|
|
62
|
+
if (typeof defaultEngine !== "string" || defaultEngine.length === 0) {
|
|
63
|
+
return { ...rest, ...(defaults !== undefined ? { defaults } : {}), configVersion: CURRENT_CONFIG_VERSION };
|
|
64
|
+
}
|
|
65
|
+
const existingDefaults = defaults !== null && typeof defaults === "object" ? defaults : {};
|
|
66
|
+
return {
|
|
67
|
+
...rest,
|
|
68
|
+
configVersion: CURRENT_CONFIG_VERSION,
|
|
69
|
+
// An explicit `defaults.llmEngine` already present in the raw 0.0.1
|
|
70
|
+
// document (should never happen for a real 0.0.1 file, but a malformed
|
|
71
|
+
// one is possible) wins over the root-level field being migrated in.
|
|
72
|
+
defaults: { llmEngine: defaultEngine, ...existingDefaults },
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
const CONFIG_VERSION_UPGRADES = {
|
|
76
|
+
"0.0.1": upgradeFrom080,
|
|
77
|
+
};
|
|
78
|
+
function unsupportedConfigVersionError(rawVersion, sourcePath) {
|
|
79
|
+
const where = sourcePath ? ` at ${sourcePath}` : "";
|
|
80
|
+
const supported = [CURRENT_CONFIG_VERSION, ...KNOWN_OLD_CONFIG_VERSIONS].map((v) => `"${v}"`).join(", ");
|
|
81
|
+
return new ConfigError(`Unsupported configVersion${where}: got ${JSON.stringify(rawVersion)}, expected one of ${supported}.`, "UNSUPPORTED_CONFIG_VERSION", "Recreate engines and improve.strategies manually for AKM 0.9.0; profile-based configuration is not translated automatically.");
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Route a raw parsed config object through the version shim before schema
|
|
85
|
+
* validation. Returns a raw object whose `configVersion` is
|
|
86
|
+
* {@link CURRENT_CONFIG_VERSION} — either unchanged (already current),
|
|
87
|
+
* in-memory-upgraded (a known old version, with a one-line stderr warning),
|
|
88
|
+
* or this throws `UNSUPPORTED_CONFIG_VERSION` (unknown, newer, missing, or
|
|
89
|
+
* malformed).
|
|
90
|
+
*/
|
|
91
|
+
export function upgradeConfigVersion(raw, sourcePath) {
|
|
92
|
+
const version = raw.configVersion;
|
|
93
|
+
if (version === CURRENT_CONFIG_VERSION)
|
|
94
|
+
return raw;
|
|
95
|
+
if (isKnownOldConfigVersion(version)) {
|
|
96
|
+
const upgraded = CONFIG_VERSION_UPGRADES[version](raw);
|
|
97
|
+
warn(`Config${sourcePath ? ` at ${sourcePath}` : ""} uses configVersion "${version}" — auto-upgraded to ${CURRENT_CONFIG_VERSION} in memory; the next config write (e.g. \`akm config set\`) persists this and silences the warning.`);
|
|
98
|
+
return upgraded;
|
|
99
|
+
}
|
|
100
|
+
throw unsupportedConfigVersionError(version, sourcePath);
|
|
101
|
+
}
|
|
@@ -8,6 +8,7 @@ import { liftLegacyEngineExtraParams } from "../extra-params.js";
|
|
|
8
8
|
import { acquireConfigLock, backupExistingConfig, parseConfigText, readConfigText, withConfigLock, writeConfigAtomic, } from "./config-io.js";
|
|
9
9
|
import { AkmConfigSchema, CURRENT_CONFIG_VERSION } from "./config-schema.js";
|
|
10
10
|
import { bundlesToSourceEntries } from "./config-sources.js";
|
|
11
|
+
import { upgradeConfigVersion } from "./config-version-shim.js";
|
|
11
12
|
import { deepMergeConfig } from "./deep-merge.js";
|
|
12
13
|
export { stripJsonComments } from "./config-io.js";
|
|
13
14
|
import { getConfigPath } from "../paths.js";
|
|
@@ -138,14 +139,13 @@ export function acquireConfigReadFence() {
|
|
|
138
139
|
* Parse raw config text and validate via Zod.
|
|
139
140
|
* ({@link AkmConfigSchema}). Returns the merged-with-defaults AkmConfig.
|
|
140
141
|
*
|
|
141
|
-
* The schema accepts only the current config version
|
|
142
|
-
*
|
|
142
|
+
* The schema accepts only the current config version. A known older version
|
|
143
|
+
* is auto-upgraded in memory first (see `./config-version-shim`); anything
|
|
144
|
+
* else — including anything newer — is rejected before the canonical shape
|
|
145
|
+
* is validated.
|
|
143
146
|
*/
|
|
144
147
|
export function parseAndValidateConfigText(text, sourcePath) {
|
|
145
|
-
const parsedRaw = parseConfigText(text, sourcePath);
|
|
146
|
-
if (parsedRaw.configVersion !== CURRENT_CONFIG_VERSION) {
|
|
147
|
-
throw new ConfigError(`Unsupported configVersion${sourcePath ? ` at ${sourcePath}` : ""}: expected "${CURRENT_CONFIG_VERSION}".`, "UNSUPPORTED_CONFIG_VERSION", "Recreate engines and improve.strategies manually for AKM 0.9.0; profile-based configuration is not translated automatically.");
|
|
148
|
-
}
|
|
148
|
+
const parsedRaw = upgradeConfigVersion(parseConfigText(text, sourcePath), sourcePath);
|
|
149
149
|
// #852 (following #815): lift legacy `extraParams` keys — e.g.
|
|
150
150
|
// `reasoning_effort`, a documented 0.9.1 workaround — onto the first-class
|
|
151
151
|
// engine field they now shadow, before the protected-key check in
|
|
@@ -456,20 +456,24 @@ function validateCommon(value) {
|
|
|
456
456
|
}
|
|
457
457
|
}
|
|
458
458
|
}
|
|
459
|
-
/**
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
459
|
+
/**
|
|
460
|
+
* Per-`schemaVersion` decoders for the persisted `improve_runs.result_json`
|
|
461
|
+
* contract, keyed by the literal `schemaVersion` value each accepts.
|
|
462
|
+
*
|
|
463
|
+
* #866 item 5 / read-shim policy (same shape as the version router in
|
|
464
|
+
* src/tasks/source/parse-task-source.ts, which this mirrors): every
|
|
465
|
+
* `schemaVersion` this build understands gets its own entry here, so
|
|
466
|
+
* decoding never regresses to "reject every row" the day the version bumps
|
|
467
|
+
* — it only stops accepting the ONE version that changed shape. All 1,539
|
|
468
|
+
* live rows today are v2; when a v3 shape is introduced, add a `3` entry
|
|
469
|
+
* (and, if v2 rows must keep reading under the newer runtime, keep this `2`
|
|
470
|
+
* entry as-is — every caller already skip-and-counts a decode failure, so a
|
|
471
|
+
* genuinely unreadable old row degrades to "excluded from metrics", never a
|
|
472
|
+
* crash). Do not remove the `2` entry when adding `3` unless the schema
|
|
473
|
+
* bump is known to be a strict superset callers can validate with it.
|
|
474
|
+
*/
|
|
475
|
+
const SCHEMA_DECODERS = {
|
|
476
|
+
2: (parsed) => {
|
|
473
477
|
requireExactFields(parsed, V2_FIELDS);
|
|
474
478
|
validateCommon(parsed);
|
|
475
479
|
if (typeof parsed.strategy !== "string" || parsed.strategy.length === 0) {
|
|
@@ -482,6 +486,23 @@ export function decodeImproveResult(input) {
|
|
|
482
486
|
envelope: parsed,
|
|
483
487
|
strategy: parsed.strategy,
|
|
484
488
|
};
|
|
489
|
+
},
|
|
490
|
+
};
|
|
491
|
+
/** Decode the persisted public result contract. */
|
|
492
|
+
export function decodeImproveResult(input) {
|
|
493
|
+
let parsed = input;
|
|
494
|
+
if (typeof input === "string") {
|
|
495
|
+
try {
|
|
496
|
+
parsed = JSON.parse(input);
|
|
497
|
+
}
|
|
498
|
+
catch {
|
|
499
|
+
fail("not valid JSON");
|
|
500
|
+
}
|
|
485
501
|
}
|
|
502
|
+
if (!isRecord(parsed))
|
|
503
|
+
fail("root must be an object");
|
|
504
|
+
const decoder = typeof parsed.schemaVersion === "number" ? SCHEMA_DECODERS[parsed.schemaVersion] : undefined;
|
|
505
|
+
if (decoder)
|
|
506
|
+
return decoder(parsed);
|
|
486
507
|
fail(`unsupported schemaVersion: ${String(parsed.schemaVersion)}`);
|
|
487
508
|
}
|
|
@@ -7,7 +7,6 @@ import path from "node:path";
|
|
|
7
7
|
import { parseFrontmatter } from "../core/asset/frontmatter.js";
|
|
8
8
|
import { UsageError } from "../core/errors.js";
|
|
9
9
|
import { createExecutionSourceIdentity } from "./source.js";
|
|
10
|
-
export const DEFAULT_GUARDED_SOURCE_MAX_BYTES = 1024 * 1024;
|
|
11
10
|
function errorMessage(cause) {
|
|
12
11
|
return cause instanceof Error ? cause.message : String(cause);
|
|
13
12
|
}
|
|
@@ -71,10 +70,6 @@ function captureRecord(sourcePathInput, containmentRootInput, options = {}) {
|
|
|
71
70
|
const sourcePath = path.resolve(sourcePathInput);
|
|
72
71
|
const { containmentRoot, containmentRealPath, containmentStat } = requireContainmentRoot(containmentRootInput);
|
|
73
72
|
const lexicalRelative = containedRelative(containmentRoot, sourcePath, false);
|
|
74
|
-
const maxBytes = options.maxBytes ?? DEFAULT_GUARDED_SOURCE_MAX_BYTES;
|
|
75
|
-
if (!Number.isSafeInteger(maxBytes) || maxBytes < 0) {
|
|
76
|
-
throw new UsageError("Guarded source byte limit must be a non-negative safe integer.", "INVALID_FLAG_VALUE");
|
|
77
|
-
}
|
|
78
73
|
const noFollow = typeof fs.constants.O_NOFOLLOW === "number" ? fs.constants.O_NOFOLLOW : 0;
|
|
79
74
|
let descriptor;
|
|
80
75
|
try {
|
|
@@ -83,9 +78,6 @@ function captureRecord(sourcePathInput, containmentRootInput, options = {}) {
|
|
|
83
78
|
if (!before.isFile()) {
|
|
84
79
|
throw new UsageError(`${sourcePath} is not a regular guarded source file.`, "INVALID_FLAG_VALUE");
|
|
85
80
|
}
|
|
86
|
-
if (before.size > BigInt(maxBytes)) {
|
|
87
|
-
throw new UsageError(`${sourcePath} exceeds the guarded source size limit (1 MiB; ${maxBytes} bytes).`, "INVALID_FLAG_VALUE");
|
|
88
|
-
}
|
|
89
81
|
const bytes = fs.readFileSync(descriptor);
|
|
90
82
|
const after = fs.fstatSync(descriptor, { bigint: true });
|
|
91
83
|
if (!sameBigIntStat(before, after) || BigInt(bytes.byteLength) !== before.size) {
|
|
@@ -127,7 +119,6 @@ function captureRecord(sourcePathInput, containmentRootInput, options = {}) {
|
|
|
127
119
|
...(options.identity ? { identity: options.identity } : {}),
|
|
128
120
|
}),
|
|
129
121
|
stat: numberStat(before),
|
|
130
|
-
maxBytes,
|
|
131
122
|
};
|
|
132
123
|
}
|
|
133
124
|
catch (cause) {
|
|
@@ -402,7 +393,6 @@ export class GuardedExecutionSourceCollector {
|
|
|
402
393
|
try {
|
|
403
394
|
current = captureRecord(record.source.sourcePath, record.source.containmentRoot, {
|
|
404
395
|
authored: record.source.authored,
|
|
405
|
-
maxBytes: record.maxBytes,
|
|
406
396
|
...(record.source.identity ? { identity: record.source.identity } : {}),
|
|
407
397
|
}).source;
|
|
408
398
|
}
|
|
@@ -6,7 +6,7 @@ import path from "node:path";
|
|
|
6
6
|
import { parseBundleRef } from "../../core/asset/asset-ref.js";
|
|
7
7
|
import { parseFrontmatter } from "../../core/asset/frontmatter.js";
|
|
8
8
|
import { asNonEmptyString } from "../../core/common.js";
|
|
9
|
-
import { isVerbose, warn } from "../../core/warn.js";
|
|
9
|
+
import { isVerbose, warn, warnVerbose } from "../../core/warn.js";
|
|
10
10
|
export const SCOPE_KEYS = ["user", "agent", "run", "channel"];
|
|
11
11
|
// ── Quality semantics (v1 spec §4.2) ────────────────────────────────────────
|
|
12
12
|
/**
|
|
@@ -1119,7 +1119,7 @@ function stripMarkdownLinkDestinations(text, nesting = 0) {
|
|
|
1119
1119
|
* secret/env/session bytes; that policy is enforced at the adapter metadata
|
|
1120
1120
|
* boundary below.
|
|
1121
1121
|
*/
|
|
1122
|
-
export function projectMarkdownContent(body) {
|
|
1122
|
+
export function projectMarkdownContent(body, truncationInfo) {
|
|
1123
1123
|
const lines = body.split(/\r?\n/);
|
|
1124
1124
|
const innerBlock = findInnerFrontmatterBlock(lines);
|
|
1125
1125
|
const start = innerBlock && isFrontmatterShaped(lines, innerBlock) ? innerBlock.close + 1 : 0;
|
|
@@ -1168,6 +1168,8 @@ export function projectMarkdownContent(body) {
|
|
|
1168
1168
|
const text = projected.join(" ").replace(/\s+/g, " ").trim();
|
|
1169
1169
|
if (!text)
|
|
1170
1170
|
return undefined;
|
|
1171
|
+
if (truncationInfo)
|
|
1172
|
+
truncationInfo.truncated = text.length > MARKDOWN_CONTENT_MAX_CHARS;
|
|
1171
1173
|
return truncateUnicodeSafe(text, MARKDOWN_CONTENT_MAX_CHARS);
|
|
1172
1174
|
}
|
|
1173
1175
|
// ── Metadata Generation ─────────────────────────────────────────────────────
|
|
@@ -1212,9 +1214,15 @@ export function applyPreContributorFields(entry, file, ctx, pkgMeta) {
|
|
|
1212
1214
|
// Native Markdown has one bounded low-weight body projection. Sensitive
|
|
1213
1215
|
// types and raw session/checkpoint material never cross this boundary.
|
|
1214
1216
|
if (entry.type !== "env" && entry.type !== "session" && !hasSessionMemoryMarker(parsed.data, parsed.content)) {
|
|
1215
|
-
const
|
|
1216
|
-
|
|
1217
|
+
const truncationInfo = { truncated: false };
|
|
1218
|
+
const contentProjection = projectMarkdownContent(parsed.content, truncationInfo);
|
|
1219
|
+
if (contentProjection) {
|
|
1217
1220
|
entry.content = contentProjection;
|
|
1221
|
+
if (truncationInfo.truncated) {
|
|
1222
|
+
entry.contentTruncated = true;
|
|
1223
|
+
warnVerbose(`${file}: indexed content truncated to ${MARKDOWN_CONTENT_MAX_CHARS} chars`);
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1218
1226
|
}
|
|
1219
1227
|
// Extract parameters from template placeholders ($1, $ARGUMENTS, {{named}})
|
|
1220
1228
|
if (entry.type === "command") {
|
|
@@ -58,6 +58,8 @@ export function indexDocumentToStashEntry(doc) {
|
|
|
58
58
|
entry.tags = doc.tags;
|
|
59
59
|
if (doc.content !== undefined)
|
|
60
60
|
entry.content = doc.content;
|
|
61
|
+
if (doc.contentTruncated !== undefined)
|
|
62
|
+
entry.contentTruncated = doc.contentTruncated;
|
|
61
63
|
if (doc.ownsPresentation !== undefined)
|
|
62
64
|
entry.ownsPresentation = doc.ownsPresentation;
|
|
63
65
|
if (doc.updated !== undefined)
|
|
@@ -1,6 +1,16 @@
|
|
|
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
|
+
* Per-field search text extraction for FTS5 indexing.
|
|
6
|
+
*
|
|
7
|
+
* Extracted from indexer.ts to break the circular dependency:
|
|
8
|
+
* db.ts -> indexer.ts -> db.ts
|
|
9
|
+
*
|
|
10
|
+
* This module imports only from metadata.ts (for the IndexDocument type),
|
|
11
|
+
* so it can be safely imported by both db.ts and indexer.ts.
|
|
12
|
+
*/
|
|
13
|
+
import { warnVerbose } from "../../core/warn.js";
|
|
4
14
|
/** Structured metadata plus bounded body text supplied to embedding providers. */
|
|
5
15
|
export const SEARCH_TEXT_MAX_CHARS = 8_192;
|
|
6
16
|
/**
|
|
@@ -80,12 +90,17 @@ export function buildSearchText(entry) {
|
|
|
80
90
|
const structured = [fields.name, fields.description, fields.tags, fields.hints]
|
|
81
91
|
.filter((field) => field.length > 0)
|
|
82
92
|
.join(" ");
|
|
83
|
-
if (structured.length >= SEARCH_TEXT_MAX_CHARS)
|
|
93
|
+
if (structured.length >= SEARCH_TEXT_MAX_CHARS) {
|
|
94
|
+
warnVerbose(`${entry.ref ?? entry.name}: search text truncated to ${SEARCH_TEXT_MAX_CHARS} chars (content dropped entirely)`);
|
|
84
95
|
return truncateUnicodeSafe(structured, SEARCH_TEXT_MAX_CHARS);
|
|
96
|
+
}
|
|
85
97
|
if (!fields.content)
|
|
86
98
|
return structured;
|
|
87
99
|
const separator = structured ? " " : "";
|
|
88
100
|
const remaining = SEARCH_TEXT_MAX_CHARS - structured.length - separator.length;
|
|
101
|
+
if (fields.content.length > remaining) {
|
|
102
|
+
warnVerbose(`${entry.ref ?? entry.name}: search text truncated to ${SEARCH_TEXT_MAX_CHARS} chars`);
|
|
103
|
+
}
|
|
89
104
|
return `${structured}${separator}${truncateUnicodeSafe(fields.content, remaining)}`;
|
|
90
105
|
}
|
|
91
106
|
function truncateUnicodeSafe(text, maxChars) {
|
|
@@ -4,6 +4,15 @@
|
|
|
4
4
|
// #484: stamp schemaVersion + shape discriminator on passthrough envelopes so
|
|
5
5
|
// third-party consumers can pin a schema version and dispatch on shape uniformly.
|
|
6
6
|
// Idempotent — never overwrites an existing schemaVersion or shape field.
|
|
7
|
+
//
|
|
8
|
+
// Builds a shallow copy rather than mutating `result` in place: several
|
|
9
|
+
// command results (e.g. `akm task sync --dry-run`'s `SchedulerPlanPreview`,
|
|
10
|
+
// see src/tasks/scheduler-sync-preview.ts) are deliberately `Object.freeze`d
|
|
11
|
+
// by their producer as an immutability guarantee, and an in-place `obj.shape
|
|
12
|
+
// = …` assignment throws ("Attempting to define property on object that is
|
|
13
|
+
// not extensible") the moment it hits one. Copying tolerates both frozen and
|
|
14
|
+
// mutable inputs uniformly, and `output()` never uses the result's identity
|
|
15
|
+
// past this call, so a copy is safe here.
|
|
7
16
|
function makeStampHandler(command) {
|
|
8
17
|
return (result) => {
|
|
9
18
|
if (result === null || result === undefined)
|
|
@@ -11,11 +20,13 @@ function makeStampHandler(command) {
|
|
|
11
20
|
if (typeof result !== "object" || Array.isArray(result))
|
|
12
21
|
return result;
|
|
13
22
|
const obj = result;
|
|
14
|
-
if (obj.shape
|
|
15
|
-
obj
|
|
16
|
-
|
|
17
|
-
obj
|
|
18
|
-
|
|
23
|
+
if (obj.shape !== undefined && obj.schemaVersion !== undefined)
|
|
24
|
+
return obj;
|
|
25
|
+
return {
|
|
26
|
+
...obj,
|
|
27
|
+
shape: obj.shape ?? command,
|
|
28
|
+
schemaVersion: obj.schemaVersion ?? 1,
|
|
29
|
+
};
|
|
19
30
|
};
|
|
20
31
|
}
|
|
21
32
|
const PASSTHROUGH_COMMANDS = [
|
|
@@ -55,6 +66,7 @@ const PASSTHROUGH_COMMANDS = [
|
|
|
55
66
|
"task-doctor",
|
|
56
67
|
"task-explain",
|
|
57
68
|
"task-history",
|
|
69
|
+
"task-prune",
|
|
58
70
|
"task-run",
|
|
59
71
|
"task-sync",
|
|
60
72
|
"task-sync-dry-run",
|