taskflow-core 0.1.5 → 0.1.6

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/dist/jsonc.js ADDED
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Zero-dependency JSONC (JSON with Comments) strip + parse.
3
+ *
4
+ * Flow definition files are hand-authored, and authors want to annotate them
5
+ * with `//` line comments and `/* block *​/` comments just like jsonc/JSON5.
6
+ * Standard `JSON.parse` rejects comments outright. This module strips comments
7
+ * (only those OUTSIDE string literals) and trailing commas before parsing, so
8
+ * a flow `.json` file may legally contain comments.
9
+ *
10
+ * Scope: this is used ONLY for user-authored flow definition files
11
+ * (readFlowFile). It must NOT be used to parse LLM/subagent output — that path
12
+ * (safeParse) stays strict by design, since model output is untrusted and we
13
+ * want structural failures to surface.
14
+ *
15
+ * The algorithm is a single linear scan that tracks whether the cursor is
16
+ * inside a string literal (respecting `\"` escapes). Comments are replaced
17
+ * with spaces so that line/column numbers in any error message stay aligned
18
+ * with the original source. Trailing commas before a closing brace/bracket
19
+ * (like `{"a": 1,}` or `[1, 2,]`) are also removed — a common hand-editing
20
+ * convenience that JSON.parse rejects.
21
+ */
22
+ /** Strip JSONC comments (// line and block) and trailing commas. Exported for testing. */
23
+ export function stripJsonComments(input) {
24
+ let out = "";
25
+ let i = 0;
26
+ const n = input.length;
27
+ while (i < n) {
28
+ const ch = input[i];
29
+ const next = input[i + 1];
30
+ // String literal — copy verbatim, honoring escape sequences. A `//` or
31
+ // `/*` that appears inside a string is part of the value, not a comment,
32
+ // and a `,` inside a string is not a trailing comma.
33
+ if (ch === '"') {
34
+ out += ch;
35
+ i++;
36
+ while (i < n) {
37
+ const c = input[i];
38
+ if (c === "\\" && i + 1 < n) {
39
+ // Keep the escape char and the escaped char together.
40
+ out += c + input[i + 1];
41
+ i += 2;
42
+ continue;
43
+ }
44
+ out += c;
45
+ i++;
46
+ if (c === '"')
47
+ break;
48
+ }
49
+ continue;
50
+ }
51
+ // Line comment `// ... \n` — drop it (do not emit a space, so line
52
+ // numbers stay aligned with the original source).
53
+ if (ch === "/" && next === "/") {
54
+ while (i < n && input[i] !== "\n")
55
+ i++;
56
+ continue;
57
+ }
58
+ // Block comment `/* ... */` — replace with a single space so that
59
+ // positions of the surrounding tokens are preserved.
60
+ if (ch === "/" && next === "*") {
61
+ i += 2;
62
+ while (i < n && !(input[i] === "*" && input[i + 1] === "/"))
63
+ i++;
64
+ i += 2; // consume the closing */
65
+ out += " ";
66
+ continue;
67
+ }
68
+ // Trailing comma: skip the comma when the next non-whitespace char is
69
+ // a closing brace or bracket. Only checked outside string literals.
70
+ // Newlines between the comma and the closer are preserved so that
71
+ // line numbers stay aligned with the original source.
72
+ if (ch === ",") {
73
+ let j = i + 1;
74
+ let sawNewline = false;
75
+ while (j < n) {
76
+ const c = input[j];
77
+ if (c === " " || c === "\t" || c === "\r") {
78
+ j++;
79
+ }
80
+ else if (c === "\n") {
81
+ sawNewline = true;
82
+ j++;
83
+ }
84
+ else {
85
+ break;
86
+ }
87
+ }
88
+ if (j < n && (input[j] === "}" || input[j] === "]")) {
89
+ if (sawNewline)
90
+ out += "\n";
91
+ i = j;
92
+ continue;
93
+ }
94
+ }
95
+ out += ch;
96
+ i++;
97
+ }
98
+ return out;
99
+ }
100
+ /**
101
+ * Parse a JSONC string (JSON with comments and trailing commas allowed) into a
102
+ * value of type T. Throws the same SyntaxError `JSON.parse` would throw for any
103
+ * other structural problem, so callers can wrap it in try/catch as usual.
104
+ */
105
+ export function parseJsonc(input) {
106
+ return JSON.parse(stripJsonComments(input));
107
+ }
108
+ //# sourceMappingURL=jsonc.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jsonc.js","sourceRoot":"","sources":["../src/jsonc.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Library — auto-derived structural metadata.
3
+ *
4
+ * Computed at save time (zero tokens, deterministic) and re-derived live at
5
+ * search time (RFC §5.2.2 staleness). No embedding logic here — that's Phase 2.
6
+ *
7
+ * Refs: docs/rfc-library-reuse.md §3.2 (phaseSignature), §3.3 (generality).
8
+ */
9
+ import type { Taskflow } from "../schema.ts";
10
+ import type { FlowMeta } from "./types.ts";
11
+ /** Topological phase-type signature, e.g. "agent→map→gate→reduce".
12
+ * Parallel same-layer phases joined with "+". Uses topoLayers() for
13
+ * deterministic ordering. */
14
+ export declare function computePhaseSignature(def: Taskflow): string;
15
+ export declare function countPhases(def: Taskflow): number;
16
+ /** Distinct agent names referenced across phases (sorted, stable). */
17
+ export declare function extractAgentUsage(def: Taskflow): string[];
18
+ /** generality ∈ [0,1]. RFC §3.3 (v2 formula, A8 fix):
19
+ * uses literalTokenRatio = literalChars/(literalChars+placeholderChars)
20
+ * so verbose-but-parameterized flows aren't structurally penalized. */
21
+ export declare function computeGenerality(def: Taskflow): number;
22
+ /** Build a fresh FlowMeta from a def + optional agent-provided fields.
23
+ * Used at save time. reuseCount starts at 0; version starts at 1. */
24
+ export declare function deriveMeta(def: Taskflow, opts: {
25
+ purpose?: string;
26
+ tags?: string[];
27
+ notes?: string;
28
+ derivedFrom?: string;
29
+ prevMeta?: FlowMeta;
30
+ }): FlowMeta;
31
+ //# sourceMappingURL=meta.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"meta.d.ts","sourceRoot":"","sources":["../../src/library/meta.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAE7C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE3C;;8BAE8B;AAC9B,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,QAAQ,GAAG,MAAM,CAM3D;AAED,wBAAgB,WAAW,CAAC,GAAG,EAAE,QAAQ,GAAG,MAAM,CAEjD;AAED,sEAAsE;AACtE,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,QAAQ,GAAG,MAAM,EAAE,CAazD;AAkFD;;wEAEwE;AACxE,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,QAAQ,GAAG,MAAM,CAYvD;AAED;sEACsE;AACtE,wBAAgB,UAAU,CACzB,GAAG,EAAE,QAAQ,EACb,IAAI,EAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,QAAQ,CAAA;CAAE,GACpG,QAAQ,CAsBV"}
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Library — auto-derived structural metadata.
3
+ *
4
+ * Computed at save time (zero tokens, deterministic) and re-derived live at
5
+ * search time (RFC §5.2.2 staleness). No embedding logic here — that's Phase 2.
6
+ *
7
+ * Refs: docs/rfc-library-reuse.md §3.2 (phaseSignature), §3.3 (generality).
8
+ */
9
+ import { topoLayers } from "../schema.js";
10
+ /** Topological phase-type signature, e.g. "agent→map→gate→reduce".
11
+ * Parallel same-layer phases joined with "+". Uses topoLayers() for
12
+ * deterministic ordering. */
13
+ export function computePhaseSignature(def) {
14
+ if (!Array.isArray(def.phases) || def.phases.length === 0)
15
+ return "";
16
+ const layers = topoLayers(def.phases);
17
+ return layers
18
+ .map((layer) => layer.map((p) => p.type ?? "agent").join("+"))
19
+ .join("→");
20
+ }
21
+ export function countPhases(def) {
22
+ return Array.isArray(def.phases) ? def.phases.length : 0;
23
+ }
24
+ /** Distinct agent names referenced across phases (sorted, stable). */
25
+ export function extractAgentUsage(def) {
26
+ if (!Array.isArray(def.phases))
27
+ return [];
28
+ const set = new Set();
29
+ for (const p of def.phases) {
30
+ if (typeof p.agent === "string" && p.agent.trim())
31
+ set.add(p.agent.trim());
32
+ // parallel branches
33
+ if (Array.isArray(p.branches)) {
34
+ for (const b of p.branches ?? []) {
35
+ if (b && typeof b.agent === "string" && b.agent.trim())
36
+ set.add(b.agent.trim());
37
+ }
38
+ }
39
+ }
40
+ return Array.from(set).sort();
41
+ }
42
+ const PLACEHOLDER_RE = /\{(args|steps|item|previous|loop|reflexion)(\.[^}]{1,200})?\}/g;
43
+ function accountContent(def) {
44
+ const acc = {
45
+ literalChars: 0,
46
+ placeholderChars: 0,
47
+ placeholderRefs: 0,
48
+ argCount: 0,
49
+ hasDescription: typeof def.description === "string" && def.description.trim().length > 0,
50
+ productionKnobs: 0,
51
+ };
52
+ // args
53
+ if (def.args && typeof def.args === "object") {
54
+ acc.argCount = Object.keys(def.args).length;
55
+ }
56
+ // production knobs (cap 0.15, each +0.05)
57
+ if (def.budget)
58
+ acc.productionKnobs += 0.05;
59
+ if (def.concurrency && typeof def.concurrency === "number")
60
+ acc.productionKnobs += 0.05;
61
+ let sawRetry = false;
62
+ let sawExpect = false;
63
+ if (Array.isArray(def.phases)) {
64
+ for (const p of def.phases) {
65
+ if (p.retry)
66
+ sawRetry = true;
67
+ if (p.expect)
68
+ sawExpect = true;
69
+ }
70
+ }
71
+ if (sawRetry)
72
+ acc.productionKnobs += 0.05;
73
+ if (sawExpect)
74
+ acc.productionKnobs += 0.05;
75
+ acc.productionKnobs = Math.min(0.15, acc.productionKnobs);
76
+ // scan phase string fields for literals vs placeholders
77
+ const scan = (s) => {
78
+ if (typeof s !== "string")
79
+ return;
80
+ PLACEHOLDER_RE.lastIndex = 0;
81
+ let m;
82
+ let lastEnd = 0;
83
+ let placeholderCharsThis = 0;
84
+ let placeholderRefsThis = 0;
85
+ while ((m = PLACEHOLDER_RE.exec(s)) !== null) {
86
+ // literal chunk before this placeholder
87
+ acc.literalChars += m.index - lastEnd;
88
+ placeholderCharsThis += m[0].length;
89
+ placeholderRefsThis += 1;
90
+ lastEnd = m.index + m[0].length;
91
+ }
92
+ acc.literalChars += s.length - lastEnd;
93
+ acc.placeholderChars += placeholderCharsThis;
94
+ acc.placeholderRefs += placeholderRefsThis;
95
+ };
96
+ if (Array.isArray(def.phases)) {
97
+ for (const p of def.phases) {
98
+ scan(p.task);
99
+ scan(p.over);
100
+ scan(p.run);
101
+ scan(p.input);
102
+ // parallel branch tasks
103
+ if (Array.isArray(p.branches)) {
104
+ for (const b of p.branches ?? []) {
105
+ scan(b?.task);
106
+ }
107
+ }
108
+ }
109
+ }
110
+ return acc;
111
+ }
112
+ /** generality ∈ [0,1]. RFC §3.3 (v2 formula, A8 fix):
113
+ * uses literalTokenRatio = literalChars/(literalChars+placeholderChars)
114
+ * so verbose-but-parameterized flows aren't structurally penalized. */
115
+ export function computeGenerality(def) {
116
+ const a = accountContent(def);
117
+ const totalChars = a.literalChars + a.placeholderChars;
118
+ const literalTokenRatio = totalChars > 0 ? a.literalChars / totalChars : 1;
119
+ let g = 0.4 * (1 - literalTokenRatio) +
120
+ 0.3 * Math.min(1, a.argCount / 3) +
121
+ 0.3 * (a.hasDescription ? 0.3 : 0) +
122
+ a.productionKnobs;
123
+ if (g < 0)
124
+ g = 0;
125
+ if (g > 1)
126
+ g = 1;
127
+ return Math.round(g * 100) / 100;
128
+ }
129
+ /** Build a fresh FlowMeta from a def + optional agent-provided fields.
130
+ * Used at save time. reuseCount starts at 0; version starts at 1. */
131
+ export function deriveMeta(def, opts) {
132
+ const now = Date.now();
133
+ const prev = opts.prevMeta;
134
+ return {
135
+ schemaVersion: 1,
136
+ purpose: opts.purpose,
137
+ tags: opts.tags,
138
+ notes: opts.notes,
139
+ phaseSignature: computePhaseSignature(def),
140
+ phaseCount: countPhases(def),
141
+ agentUsage: extractAgentUsage(def),
142
+ generality: computeGenerality(def),
143
+ reuseCount: prev?.reuseCount ?? 0,
144
+ lastUsedAt: prev?.lastUsedAt ?? null,
145
+ createdAt: prev?.createdAt ?? now,
146
+ version: prev ? prev.version + 1 : 1,
147
+ derivedFrom: opts.derivedFrom,
148
+ embedding: prev?.embedding ?? null,
149
+ embeddingModel: prev?.embeddingModel,
150
+ embeddingDim: prev?.embeddingDim,
151
+ embeddedAt: prev?.embeddedAt ?? null,
152
+ };
153
+ }
154
+ //# sourceMappingURL=meta.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"meta.js","sourceRoot":"","sources":["../../src/library/meta.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Library — search ranking (structural + keyword; Phase 1).
3
+ *
4
+ * Phase 2 adds the embedding/cosine path; the seam (LibraryDeps.embedder) is
5
+ * already in place but left unused here. When no embedder is configured OR
6
+ * structureOnly is set, search degrades to keyword + structural ranking.
7
+ *
8
+ * Refs: docs/rfc-library-reuse.md §5.2 (blend), §5.2.1 (structScore),
9
+ * §5.2.2 (staleness), §5.3 (why/reuseHint templates).
10
+ */
11
+ import { getFlow, readMeta } from "../store.ts";
12
+ import type { Taskflow } from "../schema.ts";
13
+ import type { LibraryDeps, ResolvedCandidate, SearchInput, SearchResponse } from "./types.ts";
14
+ /** Standard Levenshtein edit distance (insert/delete/replace cost 1).
15
+ * O(m·n) DP; signature strings are short (<50 chars) so cost is negligible. */
16
+ export declare function levenshtein(a: string, b: string): number;
17
+ /** Resolve a saved flow + its sidecar into a candidate whose structural fields
18
+ * are ALWAYS freshly derived from the current def, and whose embedding is only
19
+ * trusted when the sidecar's phaseSignature matches the fresh one. */
20
+ export declare function resolveCandidate(def: Taskflow, scope: "user" | "project", name: string, sidecar: ReturnType<typeof readMeta>): ResolvedCandidate;
21
+ /** Search the library. Returns ranked top-N. Degrades to structural/keyword
22
+ * when no embedder is configured or structureOnly is requested. */
23
+ export declare function searchLibrary(deps: LibraryDeps, input: SearchInput): Promise<SearchResponse>;
24
+ /** Pure cosine similarity. Zero-dep. Accepts non-normalized vectors. */
25
+ export declare function cosine(a: number[], b: number[]): number;
26
+ /** Bump reuseCount for a flow by name (RFC §6 reuse flywheel).
27
+ * Phase 1: structural-only; the caller (tool handler) decides whether to
28
+ * bump based on a `reusedFromSearch` flag. Returns the new count or null if
29
+ * the flow has no sidecar yet. The actual sidecar write is done by
30
+ * `bumpReuseInSidecar` in store.ts under withLock. */
31
+ export { getFlow };
32
+ //# sourceMappingURL=search.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../src/library/search.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,OAAO,EAAa,QAAQ,EAAE,MAAM,aAAa,CAAC;AAC3D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAO7C,OAAO,KAAK,EACX,WAAW,EACX,iBAAiB,EACjB,WAAW,EACX,cAAc,EAEd,MAAM,YAAY,CAAC;AAMpB;gFACgF;AAChF,wBAAgB,WAAW,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAmBxD;AAMD;;uEAEuE;AACvE,wBAAgB,gBAAgB,CAC/B,GAAG,EAAE,QAAQ,EACb,KAAK,EAAE,MAAM,GAAG,SAAS,EACzB,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,UAAU,CAAC,OAAO,QAAQ,CAAC,GAClC,iBAAiB,CAwCnB;AAiHD;oEACoE;AACpE,wBAAsB,aAAa,CAAC,IAAI,EAAE,WAAW,EAAE,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,cAAc,CAAC,CAoFlG;AAED,wEAAwE;AACxE,wBAAgB,MAAM,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,MAAM,EAAE,GAAG,MAAM,CAgBvD;AAED;;;;uDAIuD;AACvD,OAAO,EAAE,OAAO,EAAE,CAAC"}
@@ -0,0 +1,298 @@
1
+ /**
2
+ * Library — search ranking (structural + keyword; Phase 1).
3
+ *
4
+ * Phase 2 adds the embedding/cosine path; the seam (LibraryDeps.embedder) is
5
+ * already in place but left unused here. When no embedder is configured OR
6
+ * structureOnly is set, search degrades to keyword + structural ranking.
7
+ *
8
+ * Refs: docs/rfc-library-reuse.md §5.2 (blend), §5.2.1 (structScore),
9
+ * §5.2.2 (staleness), §5.3 (why/reuseHint templates).
10
+ */
11
+ import { getFlow, listFlows, readMeta } from "../store.js";
12
+ import { computeGenerality, computePhaseSignature, countPhases, extractAgentUsage, } from "./meta.js";
13
+ // ---------------------------------------------------------------------------
14
+ // Levenshtein (RFC §5.2.1)
15
+ // ---------------------------------------------------------------------------
16
+ /** Standard Levenshtein edit distance (insert/delete/replace cost 1).
17
+ * O(m·n) DP; signature strings are short (<50 chars) so cost is negligible. */
18
+ export function levenshtein(a, b) {
19
+ if (a === b)
20
+ return 0;
21
+ if (a.length === 0)
22
+ return b.length;
23
+ if (b.length === 0)
24
+ return a.length;
25
+ const m = a.length;
26
+ const n = b.length;
27
+ // Two rolling rows; allocate fresh each call (signatures are tiny).
28
+ let prev = new Array(n + 1);
29
+ for (let j = 0; j <= n; j++)
30
+ prev[j] = j;
31
+ for (let i = 1; i <= m; i++) {
32
+ const curr = new Array(n + 1);
33
+ curr[0] = i;
34
+ for (let j = 1; j <= n; j++) {
35
+ const cost = a.charCodeAt(i - 1) === b.charCodeAt(j - 1) ? 0 : 1;
36
+ curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
37
+ }
38
+ prev = curr;
39
+ }
40
+ return prev[n];
41
+ }
42
+ // ---------------------------------------------------------------------------
43
+ // Candidate resolution (RFC §5.2.2 staleness detection)
44
+ // ---------------------------------------------------------------------------
45
+ /** Resolve a saved flow + its sidecar into a candidate whose structural fields
46
+ * are ALWAYS freshly derived from the current def, and whose embedding is only
47
+ * trusted when the sidecar's phaseSignature matches the fresh one. */
48
+ export function resolveCandidate(def, scope, name, sidecar) {
49
+ const freshSig = computePhaseSignature(def);
50
+ const freshGen = computeGenerality(def);
51
+ const freshAgents = extractAgentUsage(def);
52
+ const freshCount = countPhases(def);
53
+ if (!sidecar) {
54
+ return {
55
+ name,
56
+ scope,
57
+ def,
58
+ phaseSignature: freshSig,
59
+ phaseCount: freshCount,
60
+ agentUsage: freshAgents,
61
+ generality: freshGen,
62
+ reuseCount: 0,
63
+ version: 1,
64
+ embedding: null,
65
+ embeddingStale: true,
66
+ };
67
+ }
68
+ const sigMatch = sidecar.phaseSignature === freshSig;
69
+ return {
70
+ name,
71
+ scope,
72
+ def,
73
+ phaseSignature: freshSig,
74
+ phaseCount: freshCount,
75
+ agentUsage: freshAgents,
76
+ generality: freshGen,
77
+ purpose: sidecar.purpose,
78
+ tags: sidecar.tags,
79
+ notes: sidecar.notes,
80
+ reuseCount: sidecar.reuseCount ?? 0,
81
+ version: sidecar.version ?? 1,
82
+ derivedFrom: sidecar.derivedFrom,
83
+ embedding: sigMatch ? sidecar.embedding ?? null : null,
84
+ embeddingStale: !sigMatch,
85
+ };
86
+ }
87
+ // ---------------------------------------------------------------------------
88
+ // Scoring
89
+ // ---------------------------------------------------------------------------
90
+ /** structScore ∈ [0,1]. RFC §5.2.1: 0.7*sigSim + 0.3*countPenalty. */
91
+ function computeStructScore(query, cand) {
92
+ let sigSim = 0;
93
+ if (cand.phaseSignature && query.phaseSignature) {
94
+ const d = levenshtein(query.phaseSignature, cand.phaseSignature);
95
+ const denom = Math.max(query.phaseSignature.length, cand.phaseSignature.length, 1);
96
+ sigSim = 1 - d / denom;
97
+ if (sigSim < 0)
98
+ sigSim = 0;
99
+ }
100
+ let countPenalty = 0;
101
+ if (query.phaseCount > 0 && cand.phaseCount > 0) {
102
+ const diff = Math.abs(query.phaseCount - cand.phaseCount);
103
+ countPenalty = 1 - Math.min(1, diff / Math.max(query.phaseCount, cand.phaseCount));
104
+ }
105
+ return 0.7 * sigSim + 0.3 * countPenalty;
106
+ }
107
+ /** textScore ∈ [0,1]. Token-overlap between query and the flow's text fields
108
+ * (name + purpose + tags + phase task text). CJK-aware: queries tokenized on
109
+ * whitespace AND matched as substrings, so a Chinese query token like "鉴权"
110
+ * still matches a purpose containing "...缺少鉴权检查..." (which whitespace
111
+ * alone would never split out). */
112
+ function computeTextScore(queryTokens, cand) {
113
+ if (queryTokens.length === 0)
114
+ return 0;
115
+ const fieldText = ((cand.name ?? "") +
116
+ " " +
117
+ (cand.purpose ?? "") +
118
+ " " +
119
+ (cand.tags ?? []).join(" ") +
120
+ " " +
121
+ cand.phaseTaskText).toLowerCase();
122
+ if (!fieldText.trim())
123
+ return 0;
124
+ // Substring match per query token (CJK-friendly). Longer tokens weigh more
125
+ // (they carry more signal) so e.g. a 2-char Chinese term counts more than a
126
+ // 2-char English one only marginally; this keeps the [0,1] shape intuitive.
127
+ let hits = 0;
128
+ for (const t of queryTokens) {
129
+ if (!t)
130
+ continue;
131
+ if (fieldText.includes(t))
132
+ hits++;
133
+ }
134
+ return hits / queryTokens.length;
135
+ }
136
+ function tokenize(query) {
137
+ return query
138
+ .toLowerCase()
139
+ .split(/[^\p{L}\p{N}]+/u)
140
+ .filter((t) => t.length > 1);
141
+ }
142
+ function phaseTaskText(def) {
143
+ if (!Array.isArray(def.phases))
144
+ return "";
145
+ const parts = [];
146
+ for (const p of def.phases) {
147
+ if (typeof p.task === "string")
148
+ parts.push(p.task);
149
+ if (Array.isArray(p.branches)) {
150
+ for (const b of p.branches ?? []) {
151
+ if (b?.task)
152
+ parts.push(b.task);
153
+ }
154
+ }
155
+ }
156
+ return parts.join(" ");
157
+ }
158
+ // ---------------------------------------------------------------------------
159
+ // why / reuseHint templates (RFC §5.3, Tier 1, zero-token)
160
+ // ---------------------------------------------------------------------------
161
+ function buildWhy(s) {
162
+ const hits = [];
163
+ if ((s.searchMode === "semantic" || s.searchMode === "mixed") && s.semScore != null && s.semScore > 0.7) {
164
+ hits.push(`语义命中(sem=${s.semScore.toFixed(2)})`);
165
+ }
166
+ if (s.structScore > 0.8)
167
+ hits.push(`结构一致(struct=${s.structScore.toFixed(2)})`);
168
+ if (s.textScore > 0.3)
169
+ hits.push(`关键词匹配(text=${s.textScore.toFixed(2)})`);
170
+ return hits.length > 0 ? hits.join(" + ") : "低分命中,建议检查";
171
+ }
172
+ function buildReuseHint(r) {
173
+ if (r.score >= 0.8)
174
+ return `直接复用${r.argCount > 0 ? `,注意 ${r.argCount} 个 args 参数` : ""}`;
175
+ if (r.score >= 0.5)
176
+ return `结构相似,建议 copy + 泛化后使用`;
177
+ return `低相关度,建议从头编写或大幅改写`;
178
+ }
179
+ // ---------------------------------------------------------------------------
180
+ // searchLibrary
181
+ // ---------------------------------------------------------------------------
182
+ function argCountOf(def) {
183
+ return def.args && typeof def.args === "object" ? Object.keys(def.args).length : 0;
184
+ }
185
+ /** Search the library. Returns ranked top-N. Degrades to structural/keyword
186
+ * when no embedder is configured or structureOnly is requested. */
187
+ export async function searchLibrary(deps, input) {
188
+ const limit = Math.min(20, Math.max(1, input.limit ?? 5));
189
+ const minScore = input.minScore ?? 0;
190
+ const scope = input.scope ?? deps.settings.scope ?? "both";
191
+ // gather candidates
192
+ const flows = listFlows(deps.cwd).filter((f) => {
193
+ if (scope === "both")
194
+ return true;
195
+ return f.scope === scope;
196
+ });
197
+ const queryTokens = tokenize(input.query);
198
+ // embed the query if an embedder is available and not structureOnly
199
+ let queryVec = null;
200
+ const hasEmbedder = !!deps.embedder && !input.structureOnly;
201
+ if (hasEmbedder && deps.embedder) {
202
+ try {
203
+ queryVec = await deps.embedder.embed(input.query);
204
+ }
205
+ catch {
206
+ queryVec = null; // degrade
207
+ }
208
+ }
209
+ const scored = [];
210
+ for (const f of flows) {
211
+ const sidecar = readMeta(deps.cwd, f.name);
212
+ const cand = resolveCandidate(f.def, f.scope, f.name, sidecar);
213
+ const qShape = {
214
+ phaseSignature: input.phaseSignatureHint ?? "",
215
+ phaseCount: input.phaseCountHint ?? 0,
216
+ };
217
+ const structScore = computeStructScore(qShape, cand);
218
+ const textScore = computeTextScore(queryTokens, {
219
+ name: cand.name,
220
+ purpose: cand.purpose,
221
+ tags: cand.tags,
222
+ phaseTaskText: phaseTaskText(cand.def),
223
+ });
224
+ let score;
225
+ let semScore;
226
+ const hasVectors = queryVec != null && cand.embedding != null && !cand.embeddingStale;
227
+ if (hasVectors && queryVec && cand.embedding) {
228
+ semScore = cosine(queryVec, cand.embedding);
229
+ score = 0.6 * semScore + 0.25 * structScore + 0.15 * textScore;
230
+ }
231
+ else {
232
+ score = 0.6 * textScore + 0.4 * structScore;
233
+ }
234
+ if (score < minScore)
235
+ continue;
236
+ if (score <= 0)
237
+ continue; // zero-relevance results are noise; don't surface them
238
+ scored.push({ cand, score: Math.round(score * 100) / 100, semScore, structScore, textScore });
239
+ }
240
+ scored.sort((a, b) => b.score - a.score);
241
+ const top = scored.slice(0, limit);
242
+ const withVectors = top.filter((s) => s.cand.embedding != null && !s.cand.embeddingStale).length;
243
+ let searchMode;
244
+ if (input.structureOnly || !deps.embedder || withVectors === 0)
245
+ searchMode = "structural";
246
+ else if (withVectors === top.length)
247
+ searchMode = "semantic";
248
+ else
249
+ searchMode = "mixed";
250
+ const results = top.map((s) => ({
251
+ name: s.cand.name,
252
+ scope: s.cand.scope,
253
+ purpose: s.cand.purpose,
254
+ tags: s.cand.tags,
255
+ phaseSignature: s.cand.phaseSignature,
256
+ phaseCount: s.cand.phaseCount,
257
+ generality: s.cand.generality,
258
+ reuseCount: s.cand.reuseCount,
259
+ version: s.cand.version,
260
+ score: s.score,
261
+ why: buildWhy({ semScore: s.semScore, structScore: s.structScore, textScore: s.textScore, searchMode }),
262
+ reuseHint: buildReuseHint({ score: s.score, argCount: argCountOf(s.cand.def) }),
263
+ }));
264
+ return {
265
+ results,
266
+ searchMode,
267
+ embedder: deps.embedder?.model,
268
+ counts: { scanned: flows.length, withVectors },
269
+ };
270
+ }
271
+ /** Pure cosine similarity. Zero-dep. Accepts non-normalized vectors. */
272
+ export function cosine(a, b) {
273
+ const n = Math.min(a.length, b.length);
274
+ if (n === 0)
275
+ return 0;
276
+ let dot = 0;
277
+ let normA = 0;
278
+ let normB = 0;
279
+ for (let i = 0; i < n; i++) {
280
+ const x = a[i];
281
+ const y = b[i];
282
+ if (!Number.isFinite(x) || !Number.isFinite(y))
283
+ continue;
284
+ dot += x * y;
285
+ normA += x * x;
286
+ normB += y * y;
287
+ }
288
+ if (normA === 0 || normB === 0)
289
+ return 0;
290
+ return dot / (Math.sqrt(normA) * Math.sqrt(normB));
291
+ }
292
+ /** Bump reuseCount for a flow by name (RFC §6 reuse flywheel).
293
+ * Phase 1: structural-only; the caller (tool handler) decides whether to
294
+ * bump based on a `reusedFromSearch` flag. Returns the new count or null if
295
+ * the flow has no sidecar yet. The actual sidecar write is done by
296
+ * `bumpReuseInSidecar` in store.ts under withLock. */
297
+ export { getFlow };
298
+ //# sourceMappingURL=search.js.map