akm-cli 0.9.12 → 0.9.14-beta.1
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 +100 -0
- package/dist/assets/workflows/workflow-template.md +4 -0
- package/dist/commands/improve/eligibility.js +27 -15
- package/dist/commands/improve/improve.js +1 -0
- package/dist/commands/lint/base-linter.js +10 -0
- package/dist/commands/proposal/drain.js +48 -6
- package/dist/commands/proposal/proposal-cli.js +1 -0
- package/dist/commands/read/curate.js +3 -2
- package/dist/commands/read/show.js +26 -9
- package/dist/core/adapter/adapters/akm-adapter.js +5 -1
- package/dist/core/asset/markdown-fragments.js +146 -0
- package/dist/core/config/config-walker.js +7 -3
- package/dist/core/config/config.js +21 -12
- package/dist/core/config/schema/primitives.js +8 -2
- package/dist/core/errors.js +2 -0
- package/dist/core/lexical-score.js +25 -0
- package/dist/core/type-presentation.js +36 -4
- package/dist/indexer/index-written-assets.js +4 -0
- package/dist/indexer/indexer.js +5 -2
- package/dist/indexer/passes/metadata.js +64 -1
- package/dist/indexer/scan/doc-to-entry.js +3 -0
- package/dist/indexer/scan/drain-dir.js +33 -22
- package/dist/indexer/search/db-search.js +72 -14
- package/dist/indexer/search/name-match.js +35 -0
- package/dist/indexer/search/ranking-contributors.js +15 -12
- package/dist/indexer/search/ranking.js +42 -18
- package/dist/indexer/usage/show-usage.js +14 -2
- package/dist/llm/client.js +12 -8
- package/dist/llm/embedders/remote.js +3 -2
- package/dist/llm/graph-extract.js +18 -67
- package/dist/output/shapes.js +46 -1
- package/dist/output/text/proposal-format.js +5 -0
- package/dist/scripts/akm-migrate-node.js +648 -253
- package/dist/scripts/akm-migrate.js +648 -253
- package/dist/storage/repositories/index-connection.js +23 -8
- package/dist/storage/repositories/index-entries-repository.js +3 -2
- package/dist/storage/repositories/index-entry-schema.js +43 -3
- package/dist/storage/repositories/index-fts-repository.js +160 -14
- package/dist/storage/repositories/index-schema.js +8 -18
- package/dist/storage/repositories/workflow-runs-repository.js +118 -10
- package/dist/workflows/exec/run-workflow.js +1 -1
- package/dist/workflows/exec/step-work.js +41 -0
- package/dist/workflows/parser.js +1 -1
- package/dist/workflows/runtime/runs.js +29 -5
- package/docs/migration/release-notes/0.9.14.md +26 -0
- package/docs/migration/release-notes/README.md +2 -0
- package/docs/reference/cli.md +18 -0
- package/package.json +1 -1
|
@@ -0,0 +1,35 @@
|
|
|
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
|
+
import { buildLexicalQueryPlan } from "./fts-query.js";
|
|
5
|
+
/**
|
|
6
|
+
* Tokenize a display name through the same Unicode-aware lexical planner as a
|
|
7
|
+
* query. Ranking must not invent a second, punctuation-dependent name grammar.
|
|
8
|
+
*/
|
|
9
|
+
export function lexicalNameTokens(name) {
|
|
10
|
+
return buildLexicalQueryPlan(name).tokens.map((token) => token.toLowerCase());
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Structural name-token evidence. Exact tokens are always meaningful; fuzzy
|
|
14
|
+
* prefix evidence requires three code points on both sides so a question's
|
|
15
|
+
* short words or digits cannot match an opaque generated storage name.
|
|
16
|
+
*/
|
|
17
|
+
export function structuralNameTokenMatch(left, right) {
|
|
18
|
+
return (left === right ||
|
|
19
|
+
(Math.min([...left].length, [...right].length) >= 3 && (left.startsWith(right) || right.startsWith(left))));
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Match a query phrase only across complete, contiguous name tokens. This is
|
|
23
|
+
* deliberately not raw substring matching: `000` must not become name
|
|
24
|
+
* evidence merely because an opaque token happens to be `z9xq000`.
|
|
25
|
+
*/
|
|
26
|
+
export function structuralNamePhraseMatch(nameTokens, queryTokens) {
|
|
27
|
+
if (queryTokens.length === 0 || queryTokens.length > nameTokens.length)
|
|
28
|
+
return false;
|
|
29
|
+
for (let start = 0; start <= nameTokens.length - queryTokens.length; start += 1) {
|
|
30
|
+
if (queryTokens.every((queryToken, index) => structuralNameTokenMatch(nameTokens[start + index], queryToken))) {
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
4
|
import { isKnownType } from "../../core/recognition-util.js";
|
|
5
5
|
import { computeGraphBoost } from "../graph/graph-boost.js";
|
|
6
|
+
import { lexicalNameTokens, structuralNamePhraseMatch, structuralNameTokenMatch } from "./name-match.js";
|
|
6
7
|
import { attachSearchHitAttribution } from "./search-attribution.js";
|
|
7
8
|
/**
|
|
8
9
|
* Chunk 1.5 (D1.5-5) — retyped from `Record<string, number>` to a FULL
|
|
@@ -105,13 +106,11 @@ function beliefStateBoost(item) {
|
|
|
105
106
|
* stash-conventions-code-spec.md — corrections demotion).
|
|
106
107
|
*
|
|
107
108
|
* Why the additive {@link beliefStateBoost} penalties alone are not enough:
|
|
108
|
-
* keyword base scores
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
* stays clamp-pinned at 1.0 above its own correction no matter what additive
|
|
114
|
-
* penalty it receives — defeating the corrections pattern's point ("so the
|
|
109
|
+
* keyword base scores have a bounded lexical floor (`normalizeFtsScores`),
|
|
110
|
+
* while the boost sum then MULTIPLIES the base (`score *= 1 + boostSum`,
|
|
111
|
+
* {@link applyScoreContributors}). A superseded incumbent can still earn
|
|
112
|
+
* enough independent boosts to outrank its own correction, so additive
|
|
113
|
+
* penalties alone cannot guarantee the corrections pattern's point ("so the
|
|
115
114
|
* ranker demotes the stale version instead of letting it outrank your fix").
|
|
116
115
|
*
|
|
117
116
|
* The ceilings guarantee the demotion while keeping flagged entries VISIBLE:
|
|
@@ -161,11 +160,10 @@ const exactNameRankingContributor = {
|
|
|
161
160
|
if (nameBase === ctx.queryLower || nameLower === ctx.queryLower) {
|
|
162
161
|
return 2.0;
|
|
163
162
|
}
|
|
164
|
-
|
|
163
|
+
const nameTokens = lexicalNameTokens(nameBase);
|
|
164
|
+
if (structuralNamePhraseMatch(nameTokens, ctx.queryTokens))
|
|
165
165
|
return 1.0;
|
|
166
|
-
|
|
167
|
-
const nameTokens = nameBase.split(/[-_\s]+/).filter(Boolean);
|
|
168
|
-
const matchCount = ctx.queryTokens.filter((qt) => nameTokens.some((nt) => nt === qt || nt.includes(qt))).length;
|
|
166
|
+
const matchCount = ctx.queryTokens.filter((qt) => nameTokens.some((nt) => structuralNameTokenMatch(nt, qt))).length;
|
|
169
167
|
return matchCount > 0 ? Math.min(0.9, matchCount * 0.3) : 0;
|
|
170
168
|
},
|
|
171
169
|
};
|
|
@@ -245,7 +243,12 @@ const aliasRankingContributor = {
|
|
|
245
243
|
const descriptionRankingContributor = {
|
|
246
244
|
name: "description-ranking",
|
|
247
245
|
appliesTo(item) {
|
|
248
|
-
|
|
246
|
+
// A relaxed FTS query admits an OR pool. Awarding a flat +0.1 merely
|
|
247
|
+
// because of a partial description coincidence double-counts a weak
|
|
248
|
+
// signal and can outrank materially stronger BM25 body evidence. The FTS
|
|
249
|
+
// score already accounts for descriptions; retain this secondary boost
|
|
250
|
+
// only for conjunctive candidates.
|
|
251
|
+
return (item.lexicalMatch !== "relaxed" && typeof item.entry.description === "string" && item.entry.description.length > 0);
|
|
249
252
|
},
|
|
250
253
|
adjust(item, ctx) {
|
|
251
254
|
const descLower = item.entry.description?.toLowerCase() ?? "";
|
|
@@ -1,20 +1,36 @@
|
|
|
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
|
+
import { stableFtsScore } from "../../core/lexical-score.js";
|
|
4
5
|
import { getUtilityScoresByIds } from "../../storage/repositories/index-utility-repository.js";
|
|
5
6
|
import { buildLexicalQueryPlan } from "./fts-query.js";
|
|
7
|
+
import { lexicalNameTokens, structuralNameTokenMatch } from "./name-match.js";
|
|
6
8
|
import { applyBeliefStateScoreCeiling, applyScoreContributors, applyUtilityContributors, defaultRankingContributors, defaultUtilityRankingContributors, } from "./ranking-contributors.js";
|
|
9
|
+
/**
|
|
10
|
+
* Lower bounds keep a lexical hit competitive with a vector-only neighbour;
|
|
11
|
+
* the upper bound deliberately leaves room for the ranking contributors that
|
|
12
|
+
* run after retrieval (notably the bounded graph boost). This is a
|
|
13
|
+
* calibration for the one search pipeline, not a claim that BM25 is
|
|
14
|
+
* comparable across different queries or FTS tables.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Convert FTS5's negative BM25 value into the lexical contribution used by
|
|
18
|
+
* this pipeline. The transform is fixed and monotone: it depends only on a
|
|
19
|
+
* row's own BM25 value, so appending weaker candidates cannot rewrite an
|
|
20
|
+
* existing row's score. FTS5 commonly emits relevance near 1e-6 for broad
|
|
21
|
+
* queries, so first put relevance on a log scale around that observed value.
|
|
22
|
+
* The shape constant intentionally makes the curve approach its ceiling
|
|
23
|
+
* slowly: rare-term scores retain separation instead of all reading as 0.8.
|
|
24
|
+
*
|
|
25
|
+
* FTS5 produces finite non-positive values in normal operation. Keeping the
|
|
26
|
+
* defensive cases here finite makes this boundary safe if a driver or fixture
|
|
27
|
+
* hands us an invalid value: `-Infinity` is the strongest possible match,
|
|
28
|
+
* while NaN, +Infinity, and positive scores contribute no lexical evidence.
|
|
29
|
+
*/
|
|
7
30
|
export function normalizeFtsScores(results) {
|
|
8
31
|
const ftsScoreMap = new Map();
|
|
9
|
-
if (results.length === 0)
|
|
10
|
-
return ftsScoreMap;
|
|
11
|
-
const bestBm25 = results[0].bm25Score;
|
|
12
|
-
const worstBm25 = results[results.length - 1].bm25Score;
|
|
13
|
-
const range = bestBm25 - worstBm25;
|
|
14
32
|
for (const result of results) {
|
|
15
|
-
|
|
16
|
-
const ftsScore = 0.3 + normalized * 0.7;
|
|
17
|
-
ftsScoreMap.set(result.id, { score: ftsScore, result });
|
|
33
|
+
ftsScoreMap.set(result.id, { score: result.lexicalScore ?? stableFtsScore(result.bm25Score), result });
|
|
18
34
|
}
|
|
19
35
|
return ftsScoreMap;
|
|
20
36
|
}
|
|
@@ -38,6 +54,7 @@ export function combineSearchScores(options) {
|
|
|
38
54
|
itemRef: result.itemRef,
|
|
39
55
|
bundleId: result.bundleId,
|
|
40
56
|
conceptId: result.conceptId,
|
|
57
|
+
fragmentId: result.fragmentId,
|
|
41
58
|
});
|
|
42
59
|
}
|
|
43
60
|
for (const [id, cosine] of options.embedScoreMap) {
|
|
@@ -98,9 +115,8 @@ export function applyRankingRules(options) {
|
|
|
98
115
|
applyRelaxedLexicalScoreCeiling(item, queryTokens);
|
|
99
116
|
// SPEC-5: demoting belief states (superseded/contradicted/archived/
|
|
100
117
|
// deprecated) cap the FINAL score. The additive belief penalty inside the
|
|
101
|
-
// multiplicative boost sum
|
|
102
|
-
//
|
|
103
|
-
// keyword match outranks its own correction forever.
|
|
118
|
+
// multiplicative boost sum can still overwhelm an additive belief penalty,
|
|
119
|
+
// so without the ceiling a superseded incumbent can outrank its correction.
|
|
104
120
|
applyBeliefStateScoreCeiling(item);
|
|
105
121
|
}
|
|
106
122
|
return options.items;
|
|
@@ -115,14 +131,12 @@ export function lexicalNameMatchTier(entry, queryTokens) {
|
|
|
115
131
|
if (queryTokens.length === 0)
|
|
116
132
|
return 0;
|
|
117
133
|
const nameBase = entry.name.toLowerCase().split("/").pop() ?? entry.name.toLowerCase();
|
|
118
|
-
const nameTokens =
|
|
119
|
-
const tokenMatches = (left, right) => left === right ||
|
|
120
|
-
(Math.min([...left].length, [...right].length) >= 3 && (left.startsWith(right) || right.startsWith(left)));
|
|
134
|
+
const nameTokens = lexicalNameTokens(nameBase);
|
|
121
135
|
if (nameTokens.length === queryTokens.length &&
|
|
122
|
-
nameTokens.every((token, index) =>
|
|
136
|
+
nameTokens.every((token, index) => structuralNameTokenMatch(token, queryTokens[index]))) {
|
|
123
137
|
return 3;
|
|
124
138
|
}
|
|
125
|
-
const matched = queryTokens.filter((token) => nameTokens.some((nameToken) =>
|
|
139
|
+
const matched = queryTokens.filter((token) => nameTokens.some((nameToken) => structuralNameTokenMatch(nameToken, token))).length;
|
|
126
140
|
if (matched === queryTokens.length)
|
|
127
141
|
return 2;
|
|
128
142
|
return matched > 0 ? 1 : 0;
|
|
@@ -130,10 +144,20 @@ export function lexicalNameMatchTier(entry, queryTokens) {
|
|
|
130
144
|
/**
|
|
131
145
|
* A relaxed OR query admits intentionally weak candidates. Candidates with no
|
|
132
146
|
* query token in their name remain visible for body-only recall, but cannot
|
|
133
|
-
*
|
|
147
|
+
* share the same bounded displayed score as stronger name-bearing recoveries.
|
|
148
|
+
* The raw ceiling is 0.65; the public score projection is applied later, so
|
|
149
|
+
* callers never literally receive `0.65` just because this ceiling bound.
|
|
150
|
+
*
|
|
151
|
+
* Preserve the pre-ceiling relevance separately from `preCeilingScore`, which
|
|
152
|
+
* belongs to belief-state demotion and may be written afterwards. A relaxed,
|
|
153
|
+
* belief-demoted candidate otherwise loses both its body relevance and its
|
|
154
|
+
* ordering signal when the second ceiling overwrites the first.
|
|
134
155
|
*/
|
|
135
156
|
function applyRelaxedLexicalScoreCeiling(item, queryTokens) {
|
|
136
157
|
if (item.lexicalMatch !== "relaxed" || lexicalNameMatchTier(item.entry, queryTokens) > 0)
|
|
137
158
|
return;
|
|
138
|
-
|
|
159
|
+
if (item.score > RELAXED_NON_NAME_SCORE_CEILING) {
|
|
160
|
+
item.preRelaxedCeilingScore = item.score;
|
|
161
|
+
item.score = RELAXED_NON_NAME_SCORE_CEILING;
|
|
162
|
+
}
|
|
139
163
|
}
|
|
@@ -22,6 +22,18 @@ export function recentShowCount(ref) {
|
|
|
22
22
|
return 0;
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
|
+
/** Compare search/display selectors by their durable parent asset identity. */
|
|
26
|
+
function traceParentRef(ref) {
|
|
27
|
+
try {
|
|
28
|
+
const parsed = parseBundleRef(ref);
|
|
29
|
+
return makeBundleRef(parsed.bundle, parsed.conceptId);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// Registry result refs are not local bundle refs and cannot have a local
|
|
33
|
+
// selector. Keep their existing exact comparison behavior.
|
|
34
|
+
return ref;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
25
37
|
function appendShowTrace(ref, type, name) {
|
|
26
38
|
appendEvent({ eventType: "show", ref, metadata: { type, name } });
|
|
27
39
|
try {
|
|
@@ -34,7 +46,7 @@ function appendShowTrace(ref, type, name) {
|
|
|
34
46
|
if (!event.ts || new Date(event.ts).getTime() < cutoffMs)
|
|
35
47
|
return false;
|
|
36
48
|
const refs = event.metadata?.resultRefs ?? [];
|
|
37
|
-
return refs.
|
|
49
|
+
return refs.some((candidate) => traceParentRef(candidate) === ref);
|
|
38
50
|
});
|
|
39
51
|
if (!matchingSearch)
|
|
40
52
|
return;
|
|
@@ -45,7 +57,7 @@ function appendShowTrace(ref, type, name) {
|
|
|
45
57
|
metadata: {
|
|
46
58
|
query: matchingSearch.metadata?.query,
|
|
47
59
|
searchTs: matchingSearch.ts,
|
|
48
|
-
rankPosition: resultRefs.
|
|
60
|
+
rankPosition: resultRefs.findIndex((candidate) => traceParentRef(candidate) === ref),
|
|
49
61
|
},
|
|
50
62
|
});
|
|
51
63
|
}
|
package/dist/llm/client.js
CHANGED
|
@@ -9,11 +9,12 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { fetchWithTimeout, readBodyWithByteCap } from "../core/common.js";
|
|
11
11
|
import { resolveSecret } from "../core/config/config.js";
|
|
12
|
-
import {
|
|
12
|
+
import { isApiKeyReference } from "../core/config/schema/primitives.js";
|
|
13
13
|
import { formatExtraParamsIssue, validateExtraParams } from "../core/extra-params.js";
|
|
14
14
|
import { redactErrorBody, redactSensitiveText } from "../core/redaction.js";
|
|
15
15
|
import { warn, warnVerbose } from "../core/warn.js";
|
|
16
16
|
import { DEFAULT_LLM_TIMEOUT_MS } from "../integrations/agent/config.js";
|
|
17
|
+
import { resolveSecretFromStore } from "../sources/snapshot-fetchers/secret-seam.js";
|
|
17
18
|
import { emitLlmUsage, extractUsageTokens, } from "./usage-telemetry.js";
|
|
18
19
|
/** Maximum length of an upstream response excerpt included in thrown errors. */
|
|
19
20
|
const ERROR_BODY_MAX_LEN = 200;
|
|
@@ -264,13 +265,16 @@ async function chatCompletionAttemptOnce(config, messages, options, timeoutMs, i
|
|
|
264
265
|
throw new Error(formatExtraParamsIssue("LLM extraParams", issue));
|
|
265
266
|
}
|
|
266
267
|
const headers = { "Content-Type": "application/json" };
|
|
267
|
-
// Resolve ONLY a whole-string
|
|
268
|
-
// hands us a materialized credential after
|
|
269
|
-
// re-running the substitution over a
|
|
270
|
-
// containing `$` — `sk-live$ecret` lost
|
|
271
|
-
// the request failed with an opaque
|
|
272
|
-
// form working for any direct
|
|
273
|
-
|
|
268
|
+
// Resolve ONLY a whole-string reference ($VAR/${VAR} or secret://<name>).
|
|
269
|
+
// The execution boundary normally hands us a materialized credential after
|
|
270
|
+
// resolving the reference upstream, so re-running the substitution over a
|
|
271
|
+
// literal key mangled any credential containing `$` — `sk-live$ecret` lost
|
|
272
|
+
// everything from the `$` onward, and the request failed with an opaque
|
|
273
|
+
// 401. The narrow check keeps the symbolic form working for any direct
|
|
274
|
+
// caller that still passes one.
|
|
275
|
+
const resolvedKey = isApiKeyReference(config.apiKey ?? "")
|
|
276
|
+
? resolveSecret(config.apiKey, resolveSecretFromStore)
|
|
277
|
+
: config.apiKey;
|
|
274
278
|
if (resolvedKey) {
|
|
275
279
|
headers.Authorization = `Bearer ${resolvedKey}`;
|
|
276
280
|
}
|
|
@@ -11,6 +11,7 @@ import { fetchWithTimeout, isHttpUrl, readBodyWithByteCap } from "../../core/com
|
|
|
11
11
|
import { resolveSecret } from "../../core/config/config.js";
|
|
12
12
|
import { redactErrorBody, redactSensitiveText } from "../../core/redaction.js";
|
|
13
13
|
import { warnVerbose } from "../../core/warn.js";
|
|
14
|
+
import { resolveSecretFromStore } from "../../sources/snapshot-fetchers/secret-seam.js";
|
|
14
15
|
/**
|
|
15
16
|
* Upper bound on the number of documents in one HTTP request, independent of
|
|
16
17
|
* the token budget below. Overridable via `config.batchSize`. Purely a
|
|
@@ -215,7 +216,7 @@ export class RemoteEmbedder {
|
|
|
215
216
|
}
|
|
216
217
|
buildHeaders() {
|
|
217
218
|
const headers = { "Content-Type": "application/json" };
|
|
218
|
-
const resolvedKey = resolveSecret(this.config.apiKey);
|
|
219
|
+
const resolvedKey = resolveSecret(this.config.apiKey, resolveSecretFromStore);
|
|
219
220
|
if (resolvedKey) {
|
|
220
221
|
headers.Authorization = `Bearer ${resolvedKey}`;
|
|
221
222
|
}
|
|
@@ -231,7 +232,7 @@ export class RemoteEmbedder {
|
|
|
231
232
|
* far unredacted and uncapped, at readBodyWithByteCap's 10 MB default.
|
|
232
233
|
*/
|
|
233
234
|
safeErrorBody(body) {
|
|
234
|
-
const resolvedKey = resolveSecret(this.config.apiKey);
|
|
235
|
+
const resolvedKey = resolveSecret(this.config.apiKey, resolveSecretFromStore);
|
|
235
236
|
return redactSensitiveText(redactErrorBody(body), resolvedKey ? [resolvedKey] : []);
|
|
236
237
|
}
|
|
237
238
|
}
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
*/
|
|
21
21
|
import systemPromptTemplate from "../assets/prompts/graph-extract-system.md" with { type: "text" };
|
|
22
22
|
import userPromptTemplate from "../assets/prompts/graph-extract-user-prompt.md" with { type: "text" };
|
|
23
|
+
import { splitMarkdownFragmentStats } from "../core/asset/markdown-fragments.js";
|
|
23
24
|
import { toErrorMessage } from "../core/common.js";
|
|
24
25
|
import { ConfigError } from "../core/errors.js";
|
|
25
26
|
import { parseEmbeddedJsonResponse } from "../core/parse.js";
|
|
@@ -117,79 +118,29 @@ function normalizeBatchState(state) {
|
|
|
117
118
|
state.nonArrayBatchFailures = Math.max(0, state.nonArrayBatchFailures ?? 0);
|
|
118
119
|
return state;
|
|
119
120
|
}
|
|
120
|
-
function splitParagraph(text, maxChars) {
|
|
121
|
-
if (text.length <= maxChars)
|
|
122
|
-
return { chunks: [text], truncationCount: 0 };
|
|
123
|
-
const chunks = [];
|
|
124
|
-
let truncationCount = 0;
|
|
125
|
-
let remaining = text;
|
|
126
|
-
while (remaining.length > maxChars) {
|
|
127
|
-
let splitAt = remaining.lastIndexOf(" ", maxChars);
|
|
128
|
-
if (splitAt < Math.floor(maxChars * 0.6))
|
|
129
|
-
splitAt = maxChars;
|
|
130
|
-
const piece = remaining.slice(0, splitAt).trim();
|
|
131
|
-
if (piece)
|
|
132
|
-
chunks.push(piece);
|
|
133
|
-
remaining = remaining.slice(splitAt).trim();
|
|
134
|
-
truncationCount += 1;
|
|
135
|
-
}
|
|
136
|
-
if (remaining)
|
|
137
|
-
chunks.push(remaining);
|
|
138
|
-
return { chunks, truncationCount };
|
|
139
|
-
}
|
|
140
121
|
function splitBodyIntoChunks(body, maxChars = MAX_CHUNK_BODY_CHARS) {
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
122
|
+
const split = splitMarkdownFragmentStats(body, maxChars);
|
|
123
|
+
// Graph extraction keeps its historical cost shape: adjacent safe fragments
|
|
124
|
+
// share one LLM call whenever they fit. The fragment splitter is still the
|
|
125
|
+
// sole boundary authority; this is only prompt packing, never a second
|
|
126
|
+
// parser/chunker. Hard splits stay isolated and telemetry remains the core
|
|
127
|
+
// split count rather than counting ordinary heading boundaries.
|
|
147
128
|
const chunks = [];
|
|
148
129
|
let current = "";
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
chunks.push(trimmed);
|
|
154
|
-
current = "";
|
|
155
|
-
};
|
|
156
|
-
for (const section of sections) {
|
|
157
|
-
if (section.length <= maxChars) {
|
|
158
|
-
const candidate = current ? `${current}\n\n${section}` : section;
|
|
159
|
-
if (candidate.length <= maxChars)
|
|
160
|
-
current = candidate;
|
|
161
|
-
else {
|
|
162
|
-
flush();
|
|
163
|
-
current = section;
|
|
164
|
-
}
|
|
165
|
-
continue;
|
|
130
|
+
for (const fragment of split.fragments) {
|
|
131
|
+
const candidate = current ? `${current}\n\n${fragment.text}` : fragment.text;
|
|
132
|
+
if (candidate.length <= maxChars) {
|
|
133
|
+
current = candidate;
|
|
166
134
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
.
|
|
171
|
-
for (const paragraph of paragraphs) {
|
|
172
|
-
if (paragraph.length <= maxChars) {
|
|
173
|
-
const candidate = current ? `${current}\n\n${paragraph}` : paragraph;
|
|
174
|
-
if (candidate.length <= maxChars)
|
|
175
|
-
current = candidate;
|
|
176
|
-
else {
|
|
177
|
-
flush();
|
|
178
|
-
current = paragraph;
|
|
179
|
-
}
|
|
180
|
-
continue;
|
|
181
|
-
}
|
|
182
|
-
flush();
|
|
183
|
-
const split = splitParagraph(paragraph, maxChars);
|
|
184
|
-
truncationCount += split.truncationCount;
|
|
185
|
-
for (const piece of split.chunks) {
|
|
186
|
-
if (piece.length <= maxChars)
|
|
187
|
-
chunks.push(piece);
|
|
188
|
-
}
|
|
135
|
+
else {
|
|
136
|
+
if (current)
|
|
137
|
+
chunks.push(current);
|
|
138
|
+
current = fragment.text;
|
|
189
139
|
}
|
|
190
140
|
}
|
|
191
|
-
|
|
192
|
-
|
|
141
|
+
if (current)
|
|
142
|
+
chunks.push(current);
|
|
143
|
+
return { chunks, truncationCount: split.hardSplitCount };
|
|
193
144
|
}
|
|
194
145
|
/** Consistency weight for blending chunk-agreement with LLM confidence. */
|
|
195
146
|
const CONSISTENCY_WEIGHT = 0.4;
|
package/dist/output/shapes.js
CHANGED
|
@@ -66,6 +66,51 @@ registerOutputShapes(BUILT_IN_OUTPUT_SHAPES);
|
|
|
66
66
|
* for a soon-frozen contract, not a silent fallback to `human`).
|
|
67
67
|
*/
|
|
68
68
|
const SHAPE_SUMMARY_COMMANDS = new Set(["show"]);
|
|
69
|
+
// ── `results` collection alias ──────────────────────────────────────────────
|
|
70
|
+
//
|
|
71
|
+
// Every list-returning command names its collection differently (`hits`,
|
|
72
|
+
// `items`, `proposals`, `sources`, ...) — a caller cannot write one accessor
|
|
73
|
+
// across commands without a per-command lookup table, and the wrong guess
|
|
74
|
+
// (`d.get("hits")` against a `curate` response) reads as "no results" rather
|
|
75
|
+
// than "wrong key", with nothing in the envelope to correct it.
|
|
76
|
+
//
|
|
77
|
+
// This maps each list-returning command to the field already holding its
|
|
78
|
+
// collection, and `withResultsAlias` below adds a `results` key pointing at
|
|
79
|
+
// that SAME array (not a copy) to the shaped output — in every `--shape` /
|
|
80
|
+
// `--detail` combination, `human` included, so `--shape agent` needs no
|
|
81
|
+
// separate handling to "guarantee" it. A new list-returning command MUST add
|
|
82
|
+
// an entry here; there is no way to detect a missed one automatically, so the
|
|
83
|
+
// survey deliberately lives in this one place rather than scattered per
|
|
84
|
+
// handler.
|
|
85
|
+
const LIST_RESULT_COLLECTION_KEYS = {
|
|
86
|
+
search: "hits",
|
|
87
|
+
curate: "items",
|
|
88
|
+
"registry-search": "hits",
|
|
89
|
+
"proposal-list": "proposals",
|
|
90
|
+
list: "sources", // `akm bundle list`
|
|
91
|
+
"env-list": "envs",
|
|
92
|
+
"secret-list": "secrets",
|
|
93
|
+
"registry-list": "registries",
|
|
94
|
+
"workflow-list": "runs",
|
|
95
|
+
"task-history": "rows",
|
|
96
|
+
"log-list": "events", // `akm log list`
|
|
97
|
+
};
|
|
98
|
+
function withResultsAlias(command, shaped) {
|
|
99
|
+
const key = LIST_RESULT_COLLECTION_KEYS[command];
|
|
100
|
+
if (!key)
|
|
101
|
+
return shaped;
|
|
102
|
+
if (shaped === null || typeof shaped !== "object" || Array.isArray(shaped))
|
|
103
|
+
return shaped;
|
|
104
|
+
const obj = shaped;
|
|
105
|
+
if ("results" in obj)
|
|
106
|
+
return shaped;
|
|
107
|
+
const collection = obj[key];
|
|
108
|
+
if (!Array.isArray(collection))
|
|
109
|
+
return shaped;
|
|
110
|
+
// Same array reference as `obj[key]`, never a copy, so `results` cannot
|
|
111
|
+
// silently drift out of sync with the semantic key it aliases.
|
|
112
|
+
return { ...obj, results: collection };
|
|
113
|
+
}
|
|
69
114
|
export function shapeForCommand(command, result, detail, shape = "human") {
|
|
70
115
|
let effectiveShape = shape;
|
|
71
116
|
if (shape === "summary" && !SHAPE_SUMMARY_COMMANDS.has(command)) {
|
|
@@ -74,7 +119,7 @@ export function shapeForCommand(command, result, detail, shape = "human") {
|
|
|
74
119
|
}
|
|
75
120
|
const handler = getOutputShapeHandler(command);
|
|
76
121
|
if (handler) {
|
|
77
|
-
return handler(result, detail, effectiveShape);
|
|
122
|
+
return withResultsAlias(command, handler(result, detail, effectiveShape));
|
|
78
123
|
}
|
|
79
124
|
// v1 spec §9 (output-shape registry exhaustive): no silent JSON.stringify
|
|
80
125
|
// fallback. A missing case here is a registration bug — fail loudly so
|
|
@@ -191,6 +191,7 @@ export function formatProposalDrainPlain(r) {
|
|
|
191
191
|
const deferred = Array.isArray(r.deferred) ? r.deferred : [];
|
|
192
192
|
const skippedByCap = Array.isArray(r.skippedByCap) ? r.skippedByCap : [];
|
|
193
193
|
const staged = Array.isArray(r.staged) ? r.staged : [];
|
|
194
|
+
const failed = Array.isArray(r.failed) ? r.failed : [];
|
|
194
195
|
const prefix = r.dryRun === true ? "[dry-run] " : "";
|
|
195
196
|
const lines = [
|
|
196
197
|
`${prefix}Drained proposal queue (strategy=${String(r.strategy ?? "?")}, policy=${policy}, applyMode=${applyMode})`,
|
|
@@ -199,10 +200,14 @@ export function formatProposalDrainPlain(r) {
|
|
|
199
200
|
` deferred: ${deferred.length}`,
|
|
200
201
|
` skippedByCap: ${skippedByCap.length}`,
|
|
201
202
|
` staged: ${staged.length}`,
|
|
203
|
+
` failed: ${failed.length}`,
|
|
202
204
|
];
|
|
203
205
|
for (const d of deferred) {
|
|
204
206
|
lines.push(` - ${String(d.id ?? "?")} (${String(d.reason ?? "?")})`);
|
|
205
207
|
}
|
|
208
|
+
for (const f of failed) {
|
|
209
|
+
lines.push(` ! ${String(f.id ?? "?")} (${String(f.reason ?? "?")}): ${String(f.detail ?? "?")}`);
|
|
210
|
+
}
|
|
206
211
|
appendLoweringNotices(lines, r);
|
|
207
212
|
return lines.join("\n").trimEnd();
|
|
208
213
|
}
|