@unbrained/pm-cli 2026.9.25 → 2026.9.26
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/.agents/skills/README.md +1 -0
- package/.agents/skills/pm-developer/SKILL.md +3 -0
- package/.agents/skills/pm-test-audit/LICENSE.txt +21 -0
- package/.agents/skills/pm-test-audit/SKILL.md +103 -0
- package/.agents/skills/pm-test-audit/references/AUDIT.md +54 -0
- package/.agents/skills/pm-test-audit/references/CAMPAIGN.md +47 -0
- package/.claude-plugin/marketplace.json +2 -2
- package/CHANGELOG.md +12 -1
- package/dist/cli/register-list-query.js +2 -2
- package/dist/cli/schema/registration.js +2 -2
- package/dist/cli-bundle/bundle-manifest.json +111 -111
- package/dist/cli-bundle/chunks/{chunk-YDIGMRLZ.js → chunk-4ODHIX7N.js} +2 -2
- package/dist/cli-bundle/chunks/{chunk-5AP67ZXH.js → chunk-FOTXRNY6.js} +39 -39
- package/dist/cli-bundle/chunks/{chunk-GAU55CWI.js → chunk-GRWPZR7F.js} +3 -3
- package/dist/cli-bundle/chunks/{chunk-3IB4THWX.js → chunk-HTHTX6E6.js} +6 -6
- package/dist/cli-bundle/chunks/{chunk-JDHWZYZU.js → chunk-IYN4MJ2C.js} +73 -73
- package/dist/cli-bundle/chunks/{chunk-YABJDOOX.js → chunk-MJWKR7YA.js} +2 -2
- package/dist/cli-bundle/chunks/{chunk-GOWZEBTH.js → chunk-QQD3VRMZ.js} +2 -2
- package/dist/cli-bundle/chunks/{chunk-COFQCKF4.js → chunk-SZQCHJI4.js} +2 -2
- package/dist/cli-bundle/chunks/{chunk-P7ZSJOWH.js → chunk-TC74K7SS.js} +2 -2
- package/dist/cli-bundle/chunks/{chunk-445ARBKY.js → chunk-TYJXMT3Q.js} +2 -2
- package/dist/cli-bundle/chunks/{chunk-25KTCTBL.js → chunk-U55RYT3I.js} +2 -2
- package/dist/cli-bundle/chunks/{chunk-CZ3VJJEA.js → chunk-VMO4P5MX.js} +2 -2
- package/dist/cli-bundle/chunks/{register-list-query-IZ7ZVTDE.js → register-list-query-426QTRET.js} +4 -4
- package/dist/cli-bundle/chunks/{register-mutation-IPAZTFDZ.js → register-mutation-FLI3HHMP.js} +4 -4
- package/dist/cli-bundle/chunks/{register-operations-APN33LNI.js → register-operations-TP7ZG4MK.js} +2 -2
- package/dist/cli-bundle/chunks/{register-setup-HPHIKG5U.js → register-setup-574SIVQZ.js} +2 -2
- package/dist/cli-bundle/focused-chunks/chunk-25BBHWMH.js +20 -0
- package/dist/cli-bundle/focused-chunks/{chunk-KRIHJH5U.js → chunk-H3BI4EDO.js} +4 -4
- package/dist/cli-bundle/focused-chunks/chunk-HNHMK5JI.js +2 -0
- package/dist/cli-bundle/focused-chunks/{chunk-IXNNMP2R.js → chunk-XVBWZTU2.js} +11 -11
- package/dist/cli-bundle/main.js +2 -2
- package/dist/cli-bundle/sdk-contracts.js +1 -1
- package/dist/cli-bundle/sdk-core.js +20 -20
- package/dist/cli-bundle/sdk-governance.js +1 -1
- package/dist/cli-bundle/sdk-merge.js +45 -45
- package/dist/cli-bundle/sdk-query.js +1 -1
- package/dist/cli-bundle/sdk-runtime.js +1 -1
- package/dist/cli-bundle/sdk-testing.js +1 -1
- package/dist/cli-bundle/sdk.js +8 -8
- package/dist/core/search/embedding-batches.d.ts +47 -15
- package/dist/core/search/embedding-batches.js +1 -1
- package/dist/core/search/eval-thresholds.d.ts +14 -0
- package/dist/core/search/eval-thresholds.js +2 -0
- package/dist/core/search/eval.d.ts +4 -1
- package/dist/core/search/eval.js +1 -1
- package/dist/core/search/http-client.d.ts +7 -0
- package/dist/core/search/http-client.js +1 -1
- package/dist/sdk/eval.d.ts +19 -17
- package/dist/sdk/eval.js +1 -1
- package/dist/sdk/generated/generated-error-code-catalog-part-1.js +1 -1
- package/dist/sdk/governance/reindex.d.ts +4 -1
- package/dist/sdk/governance/reindex.js +2 -2
- package/dist/sdk/public-surface.json +1 -1
- package/dist/sdk/query.d.ts +3 -0
- package/dist/sdk/query.js +1 -1
- package/dist/sdk/runtime/schema-preview.d.ts +2 -0
- package/dist/sdk/runtime/schema-preview.js +2 -0
- package/dist/sdk/runtime/schema.js +1 -1
- package/dist/sdk/runtime-primitives.d.ts +2 -1
- package/dist/sdk/runtime-primitives.js +1 -1
- package/docs/README.md +1 -0
- package/docs/RELEASING.md +5 -1
- package/docs/SEARCH_QUALITY.md +116 -0
- package/marketplace.json +2 -2
- package/package.json +1 -1
- package/packages/pm-beads/package.json +1 -1
- package/packages/pm-calendar/package.json +1 -1
- package/packages/pm-command-kit/package.json +1 -1
- package/packages/pm-digital-twin/package.json +1 -1
- package/packages/pm-governance-audit/package.json +1 -1
- package/packages/pm-guide-shell/package.json +1 -1
- package/packages/pm-kanban/package.json +1 -1
- package/packages/pm-lifecycle-hooks/package.json +1 -1
- package/packages/pm-linked-test-adapters/package.json +1 -1
- package/packages/pm-search-advanced/package.json +1 -1
- package/packages/pm-templates/package.json +1 -1
- package/packages/pm-todos/package.json +1 -1
- package/packages/pm-vcs/package.json +1 -1
- package/plugins/pm-claude/.claude-plugin/plugin.json +1 -1
- package/plugins/pm-claude/package.json +2 -2
- package/plugins/pm-codex/.codex-plugin/plugin.json +1 -1
- package/plugins/pm-codex/package.json +2 -2
- package/dist/cli-bundle/focused-chunks/chunk-V2X37U6J.js +0 -2
- package/dist/cli-bundle/focused-chunks/chunk-Y2M4QLAA.js +0 -20
|
@@ -1,38 +1,70 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @module core/search/embedding-batches
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Bounds embedding requests independently of per-input truncation and preserves
|
|
5
|
+
* input ordering through retries and adaptive splits.
|
|
5
6
|
*/
|
|
6
7
|
import type { PmSettings } from "../../types/index.js";
|
|
7
8
|
import type { EmbeddingProviderConfig } from "./providers.js";
|
|
8
|
-
/**
|
|
9
|
+
/** Completed vectors and execution evidence returned only after every input succeeds. */
|
|
9
10
|
export interface EmbeddingBatchExecutionResult {
|
|
10
|
-
/**
|
|
11
|
+
/** One vector for each input position, including repeated input text. */
|
|
11
12
|
vectors: number[][];
|
|
12
|
-
/**
|
|
13
|
+
/** Truncation, retry, and split diagnostics; never contains source input text. */
|
|
13
14
|
warnings: string[];
|
|
15
|
+
/** Actual provider work, including retries and adaptive splits. */
|
|
16
|
+
receipt?: EmbeddingBatchReceipt;
|
|
14
17
|
}
|
|
15
|
-
/**
|
|
18
|
+
/** Bounded summary of actual provider work; histogram keys are successful request sizes. */
|
|
19
|
+
export interface EmbeddingBatchReceipt {
|
|
20
|
+
/** Provider and model used for this execution, without endpoint or credentials. */
|
|
21
|
+
provider: string;
|
|
22
|
+
/** Model requested from the provider. */
|
|
23
|
+
model: string;
|
|
24
|
+
/** Maximum encoded JSON request size, including model and escaping. */
|
|
25
|
+
max_request_bytes: number;
|
|
26
|
+
/** Whether the request ceiling was supplied by the caller or the runtime. */
|
|
27
|
+
limit_source: "caller" | "default";
|
|
28
|
+
/** Number of requests planned before adaptive splitting. */
|
|
29
|
+
planned_batches: number;
|
|
30
|
+
/** Total HTTP attempts, including unsuccessful requests. */
|
|
31
|
+
requests: number;
|
|
32
|
+
/** Requests that returned validated vectors. */
|
|
33
|
+
successful_batches: number;
|
|
34
|
+
/** Failed requests subdivided into smaller requests. */
|
|
35
|
+
split_batches: number;
|
|
36
|
+
/** Additional attempts of the same batch after a transient failure. */
|
|
37
|
+
retries: number;
|
|
38
|
+
/** Successful request count by number of input positions. */
|
|
39
|
+
batch_size_histogram: Record<string, number>;
|
|
40
|
+
/** Wall-clock duration including provider time and retry backoff. */
|
|
41
|
+
elapsed_ms: number;
|
|
42
|
+
/** Completed input positions divided by elapsed seconds. */
|
|
43
|
+
inputs_per_second: number;
|
|
44
|
+
}
|
|
45
|
+
/** Progress across planned batches; actual HTTP work is reported separately in the receipt. */
|
|
16
46
|
export interface EmbeddingBatchProgressEvent {
|
|
17
|
-
/**
|
|
47
|
+
/** One-based index of the current planned batch. */
|
|
18
48
|
batch_index: number;
|
|
19
|
-
/**
|
|
49
|
+
/** Number of planned batches before adaptive splitting. */
|
|
20
50
|
batch_total: number;
|
|
21
|
-
/**
|
|
51
|
+
/** Number of input positions in the current planned batch. */
|
|
22
52
|
batch_size: number;
|
|
23
|
-
/**
|
|
53
|
+
/** Input positions successfully embedded before or after this event. */
|
|
24
54
|
completed_inputs: number;
|
|
25
|
-
/**
|
|
55
|
+
/** Total input positions in this execution. */
|
|
26
56
|
total_inputs: number;
|
|
27
|
-
/**
|
|
57
|
+
/** Optional attempt counter for callers extending progress reporting. */
|
|
28
58
|
attempt?: number;
|
|
29
|
-
/**
|
|
59
|
+
/** Whether dispatch is starting or the complete planned batch has succeeded. */
|
|
30
60
|
phase: "start" | "complete";
|
|
31
61
|
}
|
|
32
|
-
/**
|
|
62
|
+
/** Host controls for progress reporting and transport payload bounds. */
|
|
33
63
|
export interface EmbeddingBatchExecutionOptions {
|
|
34
|
-
/**
|
|
64
|
+
/** Observe planned-batch boundaries without receiving input text. */
|
|
35
65
|
onProgress?: (event: EmbeddingBatchProgressEvent) => void;
|
|
66
|
+
/** Positive JSON payload byte ceiling, independent of per-input corpus truncation; defaults to 256 KiB. */
|
|
67
|
+
maxRequestBytes?: number;
|
|
36
68
|
}
|
|
37
|
-
/**
|
|
69
|
+
/** Execute count- and byte-bounded batches in order. Reject impossible inputs before dispatch, retry transient failures, and subdivide size or timeout failures without returning partial vectors. */
|
|
38
70
|
export declare function executeEmbeddingBatchesWithRetry(provider: EmbeddingProviderConfig, settings: PmSettings, inputs: string[], options?: EmbeddingBatchExecutionOptions): Promise<EmbeddingBatchExecutionResult>;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
!(function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=new e.Error().stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="
|
|
1
|
+
!(function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=new e.Error().stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="dff1d04a-bad4-51d7-ab46-41e49387a44d")}catch(e2){}})();import{executeEmbeddingRequest}from"./providers.js";import{SearchHttpError}from"./http-client.js";import{toErrorMessage}from"../shared/primitives.js";import{SEMANTIC_CORPUS_TRUNCATION_SUFFIX,resolveSemanticCorpusCharacterLimit}from"./corpus.js";function resolveBatchRuntime(settings,maxInputCharacters,maxRequestBytes){const batchSizeCandidate=settings.search.embedding_batch_size;const timeoutMsCandidate=settings.search.embedding_timeout_ms;const maxRetriesCandidate=settings.search.scanner_max_batch_retries;const batchSize=Number.isFinite(batchSizeCandidate)&&batchSizeCandidate>0?Math.max(1,Math.floor(batchSizeCandidate)):1;const timeoutMs=Number.isFinite(timeoutMsCandidate)&&timeoutMsCandidate>0?Math.max(1,Math.floor(timeoutMsCandidate)):3e4;const maxRetries=Number.isFinite(maxRetriesCandidate)&&maxRetriesCandidate>=0?Math.floor(maxRetriesCandidate):0;return{batchSize,timeoutMs,maxRetries,maxRequestBytes,maxInputCharacters}}function createBatches(provider,inputs,batchSize,maxRequestBytes){const batches=[];let currentBatch=[];const envelopeBytes=Buffer.byteLength(JSON.stringify({model:provider.model,input:[]}));let currentBytes=envelopeBytes;for(const input of inputs){const inputBytes=Buffer.byteLength(JSON.stringify(input.trim()));if(envelopeBytes+inputBytes>maxRequestBytes){throw new RangeError(`Embedding input exceeds request byte ceiling ${maxRequestBytes}; lower search.embedding_corpus_max_characters or raise maxRequestBytes`)}const separatorBytes=currentBatch.length>0?1:0;if(currentBatch.length>=batchSize||currentBytes+separatorBytes+inputBytes>maxRequestBytes){batches.push(currentBatch);currentBatch=[];currentBytes=envelopeBytes}currentBytes+=inputBytes+(currentBatch.length>0?1:0);currentBatch.push(input)}batches.push(currentBatch);return batches}function truncateInputForRuntime(input,maxInputCharacters){if(!Number.isFinite(maxInputCharacters)||maxInputCharacters<=0||input.length<=maxInputCharacters){return input}const keepLength=Math.max(0,maxInputCharacters-SEMANTIC_CORPUS_TRUNCATION_SUFFIX.length);return`${input.slice(0,keepLength)}${SEMANTIC_CORPUS_TRUNCATION_SUFFIX}`.slice(0,maxInputCharacters)}function isEmbeddingTimeoutError(error){const message=toErrorMessage(error).toLowerCase();return message.includes("timed out")||message.includes("timeout")}function buildEmbeddingFailureMessage(provider,batchLabel,batchSize,timeoutMs,attempts,error){const base=`Embedding batch ${batchLabel} failed after ${attempts} attempt(s): ${toErrorMessage(error)}`;const details=`provider=${provider.name} model=${provider.model} batch_size=${batchSize} timeout_ms=${timeoutMs}`;if(!isEmbeddingTimeoutError(error)){return`${base} (${details})`}return`${base} (${details}; guidance=check provider availability or lower search.embedding_batch_size, raise search.embedding_timeout_ms, or run keyword search while semantic indexing catches up)`}function resolveSplitReason(error){if(error instanceof SearchHttpError)return error.status===413?"payload_too_large":null;return isEmbeddingTimeoutError(error)?"timeout":null}async function executeBatchWithAdaptiveSplit(provider,batch,batchLabel,timeoutMs,maxRetries,warnings,receipt){let lastError=null;for(let attempt=0;attempt<=maxRetries;attempt+=1){try{receipt.requests+=1;const batchVectors=await executeEmbeddingRequest(provider,batch,{timeout_ms:timeoutMs});if(attempt>0){warnings.push(`search_embedding_batch_retry_succeeded:batch=${batchLabel}:attempt=${attempt+1}:size=${batch.length}`)}receipt.successful_batches+=1;receipt.batch_size_histogram[batch.length]=(receipt.batch_size_histogram[batch.length]??0)+1;return batchVectors}catch(error){lastError=error;const splitReason=resolveSplitReason(error);if(splitReason!==null&&batch.length>1){receipt.split_batches+=1;const midpoint=Math.ceil(batch.length/2);const left=batch.slice(0,midpoint);const right=batch.slice(midpoint);warnings.push(`search_embedding_batch_split_after_${splitReason}:batch=${batchLabel}:size=${batch.length}:parts=${left.length}|${right.length}`);return[...await executeBatchWithAdaptiveSplit(provider,left,`${batchLabel}.1`,timeoutMs,maxRetries,warnings,receipt),...await executeBatchWithAdaptiveSplit(provider,right,`${batchLabel}.2`,timeoutMs,maxRetries,warnings,receipt)]}if(attempt<maxRetries){receipt.retries+=1;const delayMs=Math.min(1e3*2**attempt,8e3);await new Promise(resolve=>setTimeout(resolve,delayMs))}}}throw new Error(buildEmbeddingFailureMessage(provider,batchLabel,batch.length,timeoutMs,maxRetries+1,lastError))}async function executeEmbeddingBatchesWithRetry(provider,settings,inputs,options={}){if(inputs.length===0){return{vectors:[],warnings:[]}}const startedAt=performance.now();const maxRequestBytes=options.maxRequestBytes??262144;if(!Number.isSafeInteger(maxRequestBytes)||maxRequestBytes<=0){throw new RangeError("maxRequestBytes must be a positive safe integer")}const receipt={provider:provider.name,model:provider.model,max_request_bytes:maxRequestBytes,limit_source:options.maxRequestBytes===void 0?"default":"caller",planned_batches:0,requests:0,successful_batches:0,split_batches:0,retries:0,batch_size_histogram:{},elapsed_ms:0,inputs_per_second:0};const warnings=[];const corpusLimitResolution=resolveSemanticCorpusCharacterLimit(provider.name,settings.search.embedding_corpus_max_characters);if(corpusLimitResolution.warning){warnings.push(corpusLimitResolution.warning)}const runtime=resolveBatchRuntime(settings,corpusLimitResolution.maxCharacters,maxRequestBytes);let truncatedInputCount=0;const normalizedInputs=inputs.map(input=>{if(input.trim().length===0)throw new TypeError("Embedding inputs must be non-empty strings");const normalized=truncateInputForRuntime(input,runtime.maxInputCharacters);if(normalized.length<input.length){truncatedInputCount+=1}return normalized});if(truncatedInputCount>0){warnings.push(`search_embedding_input_truncated:count=${truncatedInputCount}:max_characters=${runtime.maxInputCharacters}`)}const batches=createBatches(provider,normalizedInputs,runtime.batchSize,runtime.maxRequestBytes);receipt.planned_batches=batches.length;const vectors=[];let completedInputs=0;for(let batchIndex=0;batchIndex<batches.length;batchIndex+=1){const batch=batches[batchIndex];options.onProgress?.({batch_index:batchIndex+1,batch_total:batches.length,batch_size:batch.length,completed_inputs:completedInputs,total_inputs:normalizedInputs.length,phase:"start"});vectors.push(...await executeBatchWithAdaptiveSplit(provider,batch,String(batchIndex+1),runtime.timeoutMs,runtime.maxRetries,warnings,receipt));completedInputs+=batch.length;options.onProgress?.({batch_index:batchIndex+1,batch_total:batches.length,batch_size:batch.length,completed_inputs:completedInputs,total_inputs:normalizedInputs.length,phase:"complete"})}receipt.elapsed_ms=Math.max(1,performance.now()-startedAt);receipt.inputs_per_second=inputs.length*1e3/receipt.elapsed_ms;return{vectors,warnings,receipt}}export{executeEmbeddingBatchesWithRetry};
|
|
2
2
|
//# sourceMappingURL=embedding-batches.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module core/search/eval-thresholds
|
|
3
|
+
*
|
|
4
|
+
* Validates and evaluates per-query retrieval floors independently of aggregate
|
|
5
|
+
* scores. Shared by the public evaluator and repository quality gates.
|
|
6
|
+
*/
|
|
7
|
+
/** Ranking metrics eligible for a declared quality floor. */
|
|
8
|
+
export declare const EVAL_METRIC_NAMES: readonly ["ndcg", "mrr", "precision", "recall"];
|
|
9
|
+
/** Optional normalized lower bounds; omitted metrics impose no constraint. */
|
|
10
|
+
export type EvalMetricFloors = Partial<Record<typeof EVAL_METRIC_NAMES[number], number>>;
|
|
11
|
+
/** Reject malformed or misspelled floors rather than silently weakening a quality contract. */
|
|
12
|
+
export declare function parseEvalMetricFloors(value: unknown): EvalMetricFloors;
|
|
13
|
+
/** Compare unrounded metrics to validated floors and return stable, actionable violations. */
|
|
14
|
+
export declare function evaluateMetricFloors(metrics: Record<typeof EVAL_METRIC_NAMES[number], number>, floors: EvalMetricFloors): string[];
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
!(function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=new e.Error().stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="b152a753-033c-593f-8b81-feb68b0a895f")}catch(e2){}})();const EVAL_METRIC_NAMES=["ndcg","mrr","precision","recall"];function parseEvalMetricFloors(value){if(typeof value!=="object"||value===null||Array.isArray(value)){throw new TypeError("Eval minimum must be an object of metric floors")}const result={};for(const[key,floor]of Object.entries(value)){if(!EVAL_METRIC_NAMES.some(metric=>metric===key)){throw new TypeError(`Unknown eval minimum metric: ${key}`)}if(typeof floor!=="number"||!Number.isFinite(floor)||floor<0||floor>1){throw new TypeError(`Eval minimum.${key} must be a finite number in [0, 1]`)}result[key]=floor}return result}function evaluateMetricFloors(metrics,floors){const validated=parseEvalMetricFloors(floors);const violations=[];for(const metric of EVAL_METRIC_NAMES){const minimum=validated[metric];if(minimum===void 0)continue;const actual=metrics[metric];if(!Number.isFinite(actual)||actual<0||actual>1){violations.push(`${metric}:invalid`)}else if(actual<minimum){violations.push(`${metric}:${actual}<${minimum}`)}}return violations}export{EVAL_METRIC_NAMES,evaluateMetricFloors,parseEvalMetricFloors};
|
|
2
|
+
//# sourceMappingURL=eval-thresholds.js.map
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* not. This keeps the golden set cheap to curate while still supporting the
|
|
14
14
|
* graded-discount behavior of DCG (earlier hits are worth more).
|
|
15
15
|
*/
|
|
16
|
+
import { type EvalMetricFloors } from "./eval-thresholds.js";
|
|
16
17
|
/** Retrieval mode a golden query is evaluated under. Mirrors the `pm search` mode surface; omitting it on a query defers to the eval run's default mode. */
|
|
17
18
|
export type EvalSearchMode = "keyword" | "semantic" | "hybrid";
|
|
18
19
|
/** Default cutoff (`@k`) used for every metric when the caller does not override it. */
|
|
@@ -24,7 +25,7 @@ export declare const EVAL_QUERY_SET_CONTRACT: {
|
|
|
24
25
|
readonly schema: "https://schema.unbrained.dev/pm/eval-query-set/v1";
|
|
25
26
|
readonly accepted_top_level: readonly ["array", "object"];
|
|
26
27
|
readonly required_query_fields: readonly ["query", "relevant_ids"];
|
|
27
|
-
readonly optional_fields: readonly ["mode", "description"];
|
|
28
|
+
readonly optional_fields: readonly ["mode", "description", "minimum"];
|
|
28
29
|
readonly modes: readonly ["keyword", "semantic", "hybrid"];
|
|
29
30
|
};
|
|
30
31
|
/** Copy-ready minimal query set shared by help, diagnostics, and integrations. */
|
|
@@ -46,6 +47,8 @@ export interface EvalQuery {
|
|
|
46
47
|
mode?: EvalSearchMode;
|
|
47
48
|
/** Value that configures or reports description for this contract. */
|
|
48
49
|
description?: string;
|
|
50
|
+
/** Per-query floors applied before rounding, independently of the aggregate gate. */
|
|
51
|
+
minimum?: EvalMetricFloors;
|
|
49
52
|
}
|
|
50
53
|
/** A parsed, validated golden-query set. */
|
|
51
54
|
export interface EvalQuerySet {
|
package/dist/core/search/eval.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
!(function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=new e.Error().stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="
|
|
1
|
+
!(function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=new e.Error().stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="451970e8-8403-5527-b795-a873241c015e")}catch(e2){}})();import{parseEvalMetricFloors}from"./eval-thresholds.js";const DEFAULT_EVAL_K=10;const EVAL_QUERY_SET_SCHEMA_ID="https://schema.unbrained.dev/pm/eval-query-set/v1";const EVAL_QUERY_SET_CONTRACT={schema:EVAL_QUERY_SET_SCHEMA_ID,accepted_top_level:["array","object"],required_query_fields:["query","relevant_ids"],optional_fields:["mode","description","minimum"],modes:["keyword","semantic","hybrid"]};const EVAL_QUERY_SET_EXAMPLE={schema:EVAL_QUERY_SET_SCHEMA_ID,queries:[{query:"offline search",relevant_ids:["pm-75k9"],mode:"keyword"}]};class EvalQuerySetError extends Error{constructor(message){super(message);this.name="EvalQuerySetError"}}const VALID_EVAL_MODES=new Set(["keyword","semantic","hybrid"]);function dcgAtK(relevanceGains,k){let dcg=0;const limit=Math.min(k,relevanceGains.length);for(let index=0;index<limit;index+=1){dcg+=relevanceGains[index]/Math.log2(index+2)}return dcg}function ndcgAtK(rankedIds,relevantIds,k){if(relevantIds.size===0){return 0}const gains=rankedIds.map(id=>relevantIds.has(id)?1:0);const dcg=dcgAtK(gains,k);const idealGains=Array.from({length:relevantIds.size},()=>1);const idealDcg=dcgAtK(idealGains,k);return idealDcg===0?0:dcg/idealDcg}function reciprocalRankAtK(rankedIds,relevantIds,k){const limit=Math.min(k,rankedIds.length);for(let index=0;index<limit;index+=1){if(relevantIds.has(rankedIds[index])){return 1/(index+1)}}return 0}function countRelevantInTopK(rankedIds,relevantIds,k){const limit=Math.min(k,rankedIds.length);let hits=0;for(let index=0;index<limit;index+=1){if(relevantIds.has(rankedIds[index])){hits+=1}}return hits}function precisionAtK(rankedIds,relevantIds,k){if(k<=0){return 0}return countRelevantInTopK(rankedIds,relevantIds,k)/k}function recallAtK(rankedIds,relevantIds,k){if(relevantIds.size===0){return 0}return countRelevantInTopK(rankedIds,relevantIds,k)/relevantIds.size}function evaluateRanking(rankedIds,relevantIds,k){return{ndcg:ndcgAtK(rankedIds,relevantIds,k),mrr:reciprocalRankAtK(rankedIds,relevantIds,k),precision:precisionAtK(rankedIds,relevantIds,k),recall:recallAtK(rankedIds,relevantIds,k),relevant_total:relevantIds.size,retrieved_relevant:countRelevantInTopK(rankedIds,relevantIds,k)}}function aggregateEvalMetrics(perQuery){const queryCount=perQuery.length;if(queryCount===0){return{ndcg:0,mrr:0,precision:0,recall:0,query_count:0}}const sum=perQuery.reduce((accumulator,metrics)=>({ndcg:accumulator.ndcg+metrics.ndcg,mrr:accumulator.mrr+metrics.mrr,precision:accumulator.precision+metrics.precision,recall:accumulator.recall+metrics.recall}),{ndcg:0,mrr:0,precision:0,recall:0});return{ndcg:sum.ndcg/queryCount,mrr:sum.mrr/queryCount,precision:sum.precision/queryCount,recall:sum.recall/queryCount,query_count:queryCount}}function parseEvalQueryEntry(raw,index){if(typeof raw!=="object"||raw===null){throw new EvalQuerySetError(`Eval query at index ${index} must be an object`)}const entry=raw;const query=typeof entry.query==="string"?entry.query.trim():"";if(query.length===0){throw new EvalQuerySetError(`Eval query at index ${index} must have a non-empty "query" string`)}if(!Array.isArray(entry.relevant_ids)){throw new EvalQuerySetError(`Eval query at index ${index} must have a "relevant_ids" array`)}if(entry.relevant_ids.some(value=>typeof value!=="string")){throw new EvalQuerySetError(`Eval query at index ${index} must have a "relevant_ids" array of strings`)}const relevantIds=[...new Set(entry.relevant_ids.map(value=>value.trim()).filter(value=>value.length>0))];if(relevantIds.length===0){throw new EvalQuerySetError(`Eval query at index ${index} must list at least one relevant id`)}if(entry.mode!==void 0&&(typeof entry.mode!=="string"||!VALID_EVAL_MODES.has(entry.mode))){throw new EvalQuerySetError(`Eval query at index ${index} has an invalid mode (expected keyword|semantic|hybrid)`)}return{query,relevant_ids:relevantIds,...entry.minimum===void 0?{}:{minimum:parseEvalMetricFloors(entry.minimum)},...entry.mode!==void 0?{mode:entry.mode}:{},...typeof entry.description==="string"&&entry.description.trim().length>0?{description:entry.description.trim()}:{}}}function parseEvalQuerySet(raw){const rawSchema=typeof raw==="object"&&raw!==null&&!Array.isArray(raw)?raw.schema:void 0;if(rawSchema!==void 0&&rawSchema!==EVAL_QUERY_SET_SCHEMA_ID){throw new EvalQuerySetError(`Unsupported eval query set schema; expected "${EVAL_QUERY_SET_SCHEMA_ID}"`)}const rawQueries=Array.isArray(raw)?raw:typeof raw==="object"&&raw!==null?raw.queries:void 0;if(!Array.isArray(rawQueries)){throw new EvalQuerySetError('Eval query set must be an array of queries or an object with a "queries" array')}if(rawQueries.length===0){throw new EvalQuerySetError("Eval query set must contain at least one query")}return{schema:EVAL_QUERY_SET_SCHEMA_ID,queries:rawQueries.map((entry,index)=>parseEvalQueryEntry(entry,index))}}export{DEFAULT_EVAL_K,EVAL_QUERY_SET_CONTRACT,EVAL_QUERY_SET_EXAMPLE,EVAL_QUERY_SET_SCHEMA_ID,EvalQuerySetError,aggregateEvalMetrics,dcgAtK,evaluateRanking,ndcgAtK,parseEvalQuerySet,precisionAtK,recallAtK,reciprocalRankAtK};
|
|
2
2
|
//# sourceMappingURL=eval.js.map
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
/** HTTP failure with a status code that schedulers can classify without parsing provider text. */
|
|
2
|
+
export declare class SearchHttpError extends Error {
|
|
3
|
+
/** HTTP status returned by the provider, retained independently of its response body. */
|
|
4
|
+
readonly status: number;
|
|
5
|
+
/** Preserve the existing user-facing message and the machine-readable HTTP status. */
|
|
6
|
+
constructor(message: string, status: number);
|
|
7
|
+
}
|
|
1
8
|
/** Documents the search http response payload exchanged by command, SDK, and package integrations. */
|
|
2
9
|
export interface SearchHttpResponse {
|
|
3
10
|
/** Whether the operation completed without a blocking failure. */
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
!(function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=new e.Error().stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="
|
|
1
|
+
!(function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=new e.Error().stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="01b280e5-31b5-532d-b569-67aab828358a")}catch(e2){}})();import{toErrorMessage}from"../shared/primitives.js";const DEFAULT_SEARCH_HTTP_TIMEOUT_MS=3e4;class SearchHttpError extends Error{status;constructor(message,status){super(message);this.name="SearchHttpError";this.status=status}}function normalizeSearchHttpTimeoutMs(timeoutMs,requestLabel,defaultTimeoutMs=DEFAULT_SEARCH_HTTP_TIMEOUT_MS){const resolved=timeoutMs??defaultTimeoutMs;if(!Number.isFinite(resolved)||resolved<=0){throw new Error(`${requestLabel} timeout must be a positive finite number`)}return Math.floor(resolved)}function resolveSearchHttpFetcher(fetcher,requestLabel){if(fetcher){return fetcher}if(typeof globalThis.fetch==="function"){return globalThis.fetch.bind(globalThis)}throw new Error(`${requestLabel} execution requires a fetch implementation`)}async function readFailedSearchHttpResponseBody(response){try{return(await response.text()).replaceAll(/\s+/g," ").trim()}catch(error){return`(failed to read response body: ${toErrorMessage(error)})`}}async function executeSearchJsonRequest(options){const controller=new AbortController;const timeoutHandle=setTimeout(()=>{controller.abort()},options.timeoutMs);try{let response;try{response=await options.fetcher(options.endpoint,{method:options.method,headers:options.headers,body:JSON.stringify(options.body),signal:controller.signal})}catch(error){if(error instanceof Error&&error.name==="AbortError"){throw new Error(`${options.requestLabel} timed out after ${options.timeoutMs}ms`)}throw new Error(`${options.requestLabel} execution failed: ${toErrorMessage(error)}`)}if(!response.ok){const responseBody=await readFailedSearchHttpResponseBody(response);const detail=responseBody.length>0?`: ${responseBody}`:"";throw new SearchHttpError(`${options.requestLabel} failed with status ${response.status} ${response.statusText}${detail}`,response.status)}try{return await response.json()}catch(error){throw new Error(`${options.responseLabel} JSON parse failed: ${toErrorMessage(error)}`)}}finally{clearTimeout(timeoutHandle)}}export{SearchHttpError,executeSearchJsonRequest,normalizeSearchHttpTimeoutMs,readFailedSearchHttpResponseBody,resolveSearchHttpFetcher};
|
|
2
2
|
//# sourceMappingURL=http-client.js.map
|
package/dist/sdk/eval.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { GlobalOptions } from "../core/shared/command-types.js";
|
|
|
3
3
|
export { EVAL_QUERY_SET_CONTRACT, EVAL_QUERY_SET_EXAMPLE, EVAL_QUERY_SET_SCHEMA_ID, parseEvalQuerySet, } from "../core/search/eval.js";
|
|
4
4
|
/** Relative location (under the pm root) of the default golden-query set. A git-tracked, human-curated file so relevance ground truth lives alongside the tracker it evaluates. */
|
|
5
5
|
export declare const DEFAULT_EVAL_QUERIES_RELATIVE_PATH: string;
|
|
6
|
-
/**
|
|
6
|
+
/** Evaluation inputs shared by SDK hosts and the CLI adapter. */
|
|
7
7
|
export interface EvalOptions {
|
|
8
8
|
/** Default retrieval mode for queries that do not set their own (keyword|semantic|hybrid). */
|
|
9
9
|
mode?: string;
|
|
@@ -18,41 +18,43 @@ export interface EvalOptions {
|
|
|
18
18
|
}
|
|
19
19
|
/** Per-query relevance report row emitted by {@link runEval}. */
|
|
20
20
|
export interface EvalQueryReport {
|
|
21
|
-
/**
|
|
21
|
+
/** Original golden-query text used for retrieval. */
|
|
22
22
|
query: string;
|
|
23
|
-
/**
|
|
23
|
+
/** Resolved retrieval mode after applying query and run defaults. */
|
|
24
24
|
mode: EvalSearchMode;
|
|
25
|
-
/**
|
|
25
|
+
/** Number of unique items judged relevant to this query. */
|
|
26
26
|
relevant_total: number;
|
|
27
|
-
/**
|
|
27
|
+
/** Relevant items found within the ranking cutoff. */
|
|
28
28
|
retrieved_relevant: number;
|
|
29
|
-
/**
|
|
29
|
+
/** Normalized discounted cumulative gain at the selected cutoff. */
|
|
30
30
|
ndcg: number;
|
|
31
|
-
/**
|
|
31
|
+
/** Reciprocal rank of the first relevant result, or zero when absent. */
|
|
32
32
|
mrr: number;
|
|
33
|
-
/**
|
|
33
|
+
/** Fraction of positions at the selected cutoff containing relevant items. */
|
|
34
34
|
precision: number;
|
|
35
|
-
/**
|
|
35
|
+
/** Fraction of judged relevant items retrieved at the selected cutoff. */
|
|
36
36
|
recall: number;
|
|
37
|
+
/** Failed per-query floors, compared against unrounded scores. */
|
|
38
|
+
violations?: string[];
|
|
37
39
|
}
|
|
38
|
-
/**
|
|
40
|
+
/** Rounded ranking evidence and the verdict computed from unrounded metrics. */
|
|
39
41
|
export interface EvalResult {
|
|
40
|
-
/**
|
|
42
|
+
/** Maximum ranking positions scored for each query. */
|
|
41
43
|
k: number;
|
|
42
44
|
/** Number of query entries represented by this result. */
|
|
43
45
|
query_count: number;
|
|
44
|
-
/**
|
|
46
|
+
/** Equal-weight macro averages across all evaluated queries. */
|
|
45
47
|
aggregate: {
|
|
46
48
|
ndcg: number;
|
|
47
49
|
mrr: number;
|
|
48
50
|
precision: number;
|
|
49
51
|
recall: number;
|
|
50
52
|
};
|
|
51
|
-
/**
|
|
53
|
+
/** Individual query scores and any failed declared floors. */
|
|
52
54
|
queries: EvalQueryReport[];
|
|
53
55
|
/** Present only when `--fail-under` was supplied. */
|
|
54
56
|
fail_under?: number;
|
|
55
|
-
/** Whether
|
|
57
|
+
/** Whether every declared per-query floor and optional aggregate nDCG threshold passed. */
|
|
56
58
|
passed: boolean;
|
|
57
59
|
}
|
|
58
60
|
/**
|
|
@@ -60,8 +62,8 @@ export interface EvalResult {
|
|
|
60
62
|
* (default `<pmRoot>/search/eval-queries.json`, overridable via `--queries`),
|
|
61
63
|
* runs each query through {@link runSearch} at the resolved mode, scores the
|
|
62
64
|
* returned ranking with the nDCG/MRR/precision/recall metrics, and macro-averages
|
|
63
|
-
* across queries.
|
|
64
|
-
*
|
|
65
|
-
* non-zero exit code.
|
|
65
|
+
* across queries. `passed` requires every declared per-query minimum and, when
|
|
66
|
+
* supplied, the aggregate `--fail-under` threshold. The CLI maps any failed
|
|
67
|
+
* threshold to a non-zero exit code.
|
|
66
68
|
*/
|
|
67
69
|
export declare const runEval: (options: EvalOptions, global: GlobalOptions) => Promise<EvalResult>;
|
package/dist/sdk/eval.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
!(function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=new e.Error().stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="
|
|
1
|
+
!(function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=new e.Error().stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="d4020969-4ce5-5f87-ae41-f5a3dec0a197")}catch(e2){}})();import{evaluateMetricFloors}from"../core/search/eval-thresholds.js";import{assertInitializedTracker}from"./environment/tracker-preflight.js";import fs from"node:fs/promises";import path from"node:path";import{aggregateEvalMetrics,DEFAULT_EVAL_K,EVAL_QUERY_SET_EXAMPLE,EVAL_QUERY_SET_SCHEMA_ID,evaluateRanking,parseEvalQuerySet}from"../core/search/eval.js";import{EXIT_CODE}from"../core/shared/constants.js";import{PmCliError}from"../core/shared/errors.js";import{coercePositiveInteger}from"../core/shared/primitives.js";import{resolvePmRoot}from"../core/store/paths.js";import{runSearch}from"./query/search.js";import{EVAL_QUERY_SET_CONTRACT,EVAL_QUERY_SET_EXAMPLE as EVAL_QUERY_SET_EXAMPLE2,EVAL_QUERY_SET_SCHEMA_ID as EVAL_QUERY_SET_SCHEMA_ID2,parseEvalQuerySet as parseEvalQuerySet2}from"../core/search/eval.js";const DEFAULT_EVAL_QUERIES_RELATIVE_PATH=path.join("search","eval-queries.json");const EVAL_QUERY_SET_ERROR_CONTEXT={schema:EVAL_QUERY_SET_SCHEMA_ID,examples:[JSON.stringify(EVAL_QUERY_SET_EXAMPLE)],nextSteps:["Create a git-tracked golden-query JSON file that follows the schema, then re-run pm eval."]};const VALID_EVAL_MODES=new Set(["keyword","semantic","hybrid"]);const parseEvalMode=raw=>{if(raw===void 0){return"keyword"}const normalized=raw.trim().toLowerCase();if(!VALID_EVAL_MODES.has(normalized)){throw new PmCliError("Eval --mode must be one of keyword|semantic|hybrid",EXIT_CODE.USAGE)}return normalized};const parseEvalK=raw=>{if(raw===void 0||raw===""){return DEFAULT_EVAL_K}const parsed=coercePositiveInteger(raw);if(parsed===null){throw new PmCliError("Eval --k must be a positive integer",EXIT_CODE.USAGE)}return parsed};const parseFailUnder=raw=>{if(raw===void 0||raw===""){return void 0}const normalized=String(raw).trim();const parsed=Number(normalized);if([normalized.length===0,!Number.isFinite(parsed),parsed<0,parsed>1].includes(true)){throw new PmCliError("Eval --fail-under must be a number in the range [0, 1]",EXIT_CODE.USAGE)}return parsed};const roundMetric=value=>Math.round(value*1e4)/1e4;const readEvalQuerySetFile=async queriesPath=>{try{return await fs.readFile(queriesPath,"utf8")}catch{throw new PmCliError(`Eval query set not found at ${queriesPath}`,EXIT_CODE.NOT_FOUND,{...EVAL_QUERY_SET_ERROR_CONTEXT,examples:[`echo '${JSON.stringify(EVAL_QUERY_SET_EXAMPLE)}' > ${queriesPath}`,"pm eval --queries ./my-eval.json"]})}};const parseEvalQuerySetJson=(raw,queriesPath)=>{try{return JSON.parse(raw)}catch(error){throw new PmCliError(`Eval query set at ${queriesPath} is not valid JSON: ${error instanceof Error?error.message:String(error)}`,EXIT_CODE.USAGE,EVAL_QUERY_SET_ERROR_CONTEXT)}};const validateEvalQuerySet=parsed=>{try{return parseEvalQuerySet(parsed)}catch(error){throw new PmCliError(error instanceof Error?error.message:String(error),EXIT_CODE.USAGE,EVAL_QUERY_SET_ERROR_CONTEXT)}};const loadEvalQuerySet=async queriesPath=>validateEvalQuerySet(parseEvalQuerySetJson(await readEvalQuerySetFile(queriesPath),queriesPath));const evaluateEvalQuery=async(evalQuery,defaultMode,k,global2)=>{const mode=evalQuery.mode??defaultMode;const searchResult=await runSearch(evalQuery.query,{mode,limit:String(k),fields:"id"},global2);const rankedIds=searchResult.items.map(item=>item.id).filter(id=>typeof id==="string");const metrics=evaluateRanking(rankedIds,new Set(evalQuery.relevant_ids),k);return{metrics,report:{query:evalQuery.query,...evalQuery.minimum===void 0?{}:{violations:evaluateMetricFloors(metrics,evalQuery.minimum)},mode,relevant_total:metrics.relevant_total,retrieved_relevant:metrics.retrieved_relevant,ndcg:roundMetric(metrics.ndcg),mrr:roundMetric(metrics.mrr),precision:roundMetric(metrics.precision),recall:roundMetric(metrics.recall)}}};const buildEvalResult=(k,reports,rawMetrics,failUnder)=>{const aggregate=aggregateEvalMetrics(rawMetrics);return{k,query_count:reports.length,aggregate:{ndcg:roundMetric(aggregate.ndcg),mrr:roundMetric(aggregate.mrr),precision:roundMetric(aggregate.precision),recall:roundMetric(aggregate.recall)},queries:reports,...failUnder!==void 0?{fail_under:failUnder}:{},passed:(failUnder===void 0||aggregate.ndcg>=failUnder)&&reports.every(report=>(report.violations?.length??0)===0)}};const runEval=async(options,global2)=>{const pmRoot=resolvePmRoot(process.cwd(),global2.path);await assertInitializedTracker(pmRoot);const k=parseEvalK(options.k);const defaultMode=parseEvalMode(options.mode);const failUnder=parseFailUnder(options.failUnder);const queriesPath=options.queries?path.resolve(process.cwd(),options.queries):path.join(pmRoot,DEFAULT_EVAL_QUERIES_RELATIVE_PATH);const querySet=await loadEvalQuerySet(queriesPath);const reports=[];const rawMetrics=[];for(const evalQuery of querySet.queries){const{report,metrics}=await evaluateEvalQuery(evalQuery,defaultMode,k,global2);rawMetrics.push(metrics);reports.push(report)}return buildEvalResult(k,reports,rawMetrics,failUnder)};export{DEFAULT_EVAL_QUERIES_RELATIVE_PATH,EVAL_QUERY_SET_CONTRACT,EVAL_QUERY_SET_EXAMPLE2 as EVAL_QUERY_SET_EXAMPLE,EVAL_QUERY_SET_SCHEMA_ID2 as EVAL_QUERY_SET_SCHEMA_ID,parseEvalQuerySet2 as parseEvalQuerySet,runEval};
|
|
2
2
|
//# sourceMappingURL=eval.js.map
|