scoutline 0.23.0 → 0.24.0
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/README.md +49 -1
- package/dist/capabilities/investigation.d.ts +134 -0
- package/dist/capabilities/investigation.d.ts.map +1 -0
- package/dist/capabilities/investigation.js +278 -0
- package/dist/capabilities/investigation.js.map +1 -0
- package/dist/commands/investigate.d.ts +232 -0
- package/dist/commands/investigate.d.ts.map +1 -0
- package/dist/commands/investigate.js +838 -0
- package/dist/commands/investigate.js.map +1 -0
- package/dist/commands/vision.d.ts.map +1 -1
- package/dist/commands/vision.js +15 -2
- package/dist/commands/vision.js.map +1 -1
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +373 -6
- package/dist/index.js.map +1 -1
- package/dist/lib/investigate-claims.d.ts +90 -0
- package/dist/lib/investigate-claims.d.ts.map +1 -0
- package/dist/lib/investigate-claims.js +188 -0
- package/dist/lib/investigate-claims.js.map +1 -0
- package/dist/lib/investigate-extract.d.ts +35 -0
- package/dist/lib/investigate-extract.d.ts.map +1 -0
- package/dist/lib/investigate-extract.js +101 -0
- package/dist/lib/investigate-extract.js.map +1 -0
- package/dist/lib/investigate-planner.d.ts +58 -0
- package/dist/lib/investigate-planner.d.ts.map +1 -0
- package/dist/lib/investigate-planner.js +141 -0
- package/dist/lib/investigate-planner.js.map +1 -0
- package/dist/providers/registry.d.ts.map +1 -1
- package/dist/providers/registry.js +4 -1
- package/dist/providers/registry.js.map +1 -1
- package/dist/providers/types.d.ts +30 -1
- package/dist/providers/types.d.ts.map +1 -1
- package/dist/providers/types.js.map +1 -1
- package/dist/providers/zai/adapter.d.ts.map +1 -1
- package/dist/providers/zai/adapter.js +217 -3
- package/dist/providers/zai/adapter.js.map +1 -1
- package/dist/providers/zai/layout-parsing.d.ts +76 -0
- package/dist/providers/zai/layout-parsing.d.ts.map +1 -0
- package/dist/providers/zai/layout-parsing.js +151 -0
- package/dist/providers/zai/layout-parsing.js.map +1 -0
- package/dist/providers/zai/media.d.ts +19 -0
- package/dist/providers/zai/media.d.ts.map +1 -1
- package/dist/providers/zai/media.js +61 -0
- package/dist/providers/zai/media.js.map +1 -1
- package/package.json +1 -1
- package/skills/scoutline/SKILL.md +71 -1
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic claim splitting + claim↔evidence matching for
|
|
3
|
+
* `investigate --verify` (investigate-verify lane, Ticket T1; DESIGN
|
|
4
|
+
* D2, PRD AC-2/AC-4/AC-5).
|
|
5
|
+
*
|
|
6
|
+
* splitClaims: the extract grammar's terminator rules verbatim
|
|
7
|
+
* (lib/investigate-extract.ts splitWindows) — `[.!?]` followed by a
|
|
8
|
+
* space (or end of input), or a newline; the terminator char belongs
|
|
9
|
+
* to the window, the separator run (spaces/tabs) to neither. The
|
|
10
|
+
* splitter is PURE: the ≤ 8 claim cap is the CALLER's fail-loud
|
|
11
|
+
* validation (commands/investigate.ts, T3) — a 9-sentence statement
|
|
12
|
+
* returns 9 claims here, never a throw, never a truncation.
|
|
13
|
+
*
|
|
14
|
+
* matchClaimsToEvidence: zero-model, deterministic. Per claim —
|
|
15
|
+
* terms = normalizeTerms over the claim's stopword-filtered tokens
|
|
16
|
+
* (reused from lib/investigate-extract.ts, imports only); a passage
|
|
17
|
+
* matches when any term appears whole-word case-folded in
|
|
18
|
+
* passage.quote; the negation-cue scan runs over MATCHING passages
|
|
19
|
+
* only; evidence pointers land in first-encounter order (source
|
|
20
|
+
* order, then passage order); negationCues counts cue-bearing
|
|
21
|
+
* matching passages.
|
|
22
|
+
*
|
|
23
|
+
* Byte-stable: no clocks, no randomness, no locale-dependent casing
|
|
24
|
+
* beyond toLowerCase.
|
|
25
|
+
*/
|
|
26
|
+
import type { EvidenceSource } from "../capabilities/investigation.js";
|
|
27
|
+
/**
|
|
28
|
+
* The verify-mode claim cap (PRD AC-1: > 8 sentence-claims is a
|
|
29
|
+
* fail-loud VALIDATION_ERROR, the R1 pipe-cap precedent — fail, never
|
|
30
|
+
* truncate). Exported so the command's error message names the cap
|
|
31
|
+
* from the single source of truth.
|
|
32
|
+
*/
|
|
33
|
+
export declare const MAX_VERIFY_CLAIMS = 8;
|
|
34
|
+
/**
|
|
35
|
+
* Split a statement into sentence-claims: extract-grammar windows,
|
|
36
|
+
* whitespace-only sentences dropped, quotes trimmed. Deterministic —
|
|
37
|
+
* the same statement yields the identical claim array byte-stably.
|
|
38
|
+
* `|` needs no handling: claims split on sentences, never pipes.
|
|
39
|
+
*/
|
|
40
|
+
export declare function splitClaims(statement: string): string[];
|
|
41
|
+
/**
|
|
42
|
+
* The negation-cue list behind `contradicted`. FROZEN and versioned:
|
|
43
|
+
* additions change verdicts and are release-visible (the parent-owned
|
|
44
|
+
* CHANGELOG bullet names them). Every cue is a LITERAL whole-word
|
|
45
|
+
* string (fix-round m1: the parenthesized inflection grammar is gone —
|
|
46
|
+
* variants spell out; a missing-absence family covers "without",
|
|
47
|
+
* "lacks", "untrue", "false", "absent", "rarely", "seldom").
|
|
48
|
+
*
|
|
49
|
+
* DISCLOSED as hint-grade: `contradicted` means "a term-matching
|
|
50
|
+
* passage carries a negation cue", NOT semantic contradiction —
|
|
51
|
+
* semantic judgment stays with the calling agent (agent-synthesis
|
|
52
|
+
* contract); the per-claim cue count exists for exactly that
|
|
53
|
+
* re-judgment.
|
|
54
|
+
*/
|
|
55
|
+
export declare const NEGATION_CUES: readonly string[];
|
|
56
|
+
/** The deterministic verdict vocabulary (PRD AC-5). */
|
|
57
|
+
export type ClaimVerdict = "corroborated" | "contradicted" | "unresolved";
|
|
58
|
+
/** One evidence pointer into the pack's own sources array. */
|
|
59
|
+
export interface ClaimEvidencePointer {
|
|
60
|
+
readonly sourceIndex: number;
|
|
61
|
+
readonly passageIndex: number;
|
|
62
|
+
}
|
|
63
|
+
/** One claim row: text, verdict, cue count, evidence pointers. */
|
|
64
|
+
export interface ClaimRow {
|
|
65
|
+
readonly text: string;
|
|
66
|
+
readonly verdict: ClaimVerdict;
|
|
67
|
+
/** Number of cue-bearing MATCHING passages (agent re-judgment input). */
|
|
68
|
+
readonly negationCues: number;
|
|
69
|
+
readonly evidence: readonly ClaimEvidencePointer[];
|
|
70
|
+
}
|
|
71
|
+
/** The additive verify block (PRD AC-6): statement + per-claim rows. */
|
|
72
|
+
export interface VerifyBlock {
|
|
73
|
+
readonly statement: string;
|
|
74
|
+
readonly claims: readonly ClaimRow[];
|
|
75
|
+
}
|
|
76
|
+
export interface MatchClaimsArgs {
|
|
77
|
+
readonly statement: string;
|
|
78
|
+
readonly claims: readonly string[];
|
|
79
|
+
readonly sources: readonly EvidenceSource[];
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Match claims to evidence: per claim, every (source, passage) whose
|
|
83
|
+
* quote contains ≥ 1 claim term whole-word case-folded becomes an
|
|
84
|
+
* evidence pointer in first-encounter order; the cue scan runs over
|
|
85
|
+
* matching passages only; the verdict follows PRD-5 (corroborated =
|
|
86
|
+
* matches + 0 cues; contradicted = ≥ 1 cue-bearing match; unresolved
|
|
87
|
+
* = no matches).
|
|
88
|
+
*/
|
|
89
|
+
export declare function matchClaimsToEvidence({ statement, claims, sources }: MatchClaimsArgs): VerifyBlock;
|
|
90
|
+
//# sourceMappingURL=investigate-claims.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"investigate-claims.d.ts","sourceRoot":"","sources":["../../src/lib/investigate-claims.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAIH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kCAAkC,CAAC;AAQvE;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,IAAI,CAAC;AAgDnC;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,EAAE,CASvD;AAMD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,aAAa,EAAE,SAAS,MAAM,EAgCzC,CAAC;AAQH,uDAAuD;AACvD,MAAM,MAAM,YAAY,GAAG,cAAc,GAAG,cAAc,GAAG,YAAY,CAAC;AAE1E,8DAA8D;AAC9D,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC/B;AAED,kEAAkE;AAClE,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAC;IAC/B,yEAAyE;IACzE,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,QAAQ,EAAE,SAAS,oBAAoB,EAAE,CAAC;CACpD;AAED,wEAAwE;AACxE,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE,SAAS,QAAQ,EAAE,CAAC;CACtC;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,QAAQ,CAAC,OAAO,EAAE,SAAS,cAAc,EAAE,CAAC;CAC7C;AAuBD;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,eAAe,GAAG,WAAW,CAqBlG"}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic claim splitting + claim↔evidence matching for
|
|
3
|
+
* `investigate --verify` (investigate-verify lane, Ticket T1; DESIGN
|
|
4
|
+
* D2, PRD AC-2/AC-4/AC-5).
|
|
5
|
+
*
|
|
6
|
+
* splitClaims: the extract grammar's terminator rules verbatim
|
|
7
|
+
* (lib/investigate-extract.ts splitWindows) — `[.!?]` followed by a
|
|
8
|
+
* space (or end of input), or a newline; the terminator char belongs
|
|
9
|
+
* to the window, the separator run (spaces/tabs) to neither. The
|
|
10
|
+
* splitter is PURE: the ≤ 8 claim cap is the CALLER's fail-loud
|
|
11
|
+
* validation (commands/investigate.ts, T3) — a 9-sentence statement
|
|
12
|
+
* returns 9 claims here, never a throw, never a truncation.
|
|
13
|
+
*
|
|
14
|
+
* matchClaimsToEvidence: zero-model, deterministic. Per claim —
|
|
15
|
+
* terms = normalizeTerms over the claim's stopword-filtered tokens
|
|
16
|
+
* (reused from lib/investigate-extract.ts, imports only); a passage
|
|
17
|
+
* matches when any term appears whole-word case-folded in
|
|
18
|
+
* passage.quote; the negation-cue scan runs over MATCHING passages
|
|
19
|
+
* only; evidence pointers land in first-encounter order (source
|
|
20
|
+
* order, then passage order); negationCues counts cue-bearing
|
|
21
|
+
* matching passages.
|
|
22
|
+
*
|
|
23
|
+
* Byte-stable: no clocks, no randomness, no locale-dependent casing
|
|
24
|
+
* beyond toLowerCase.
|
|
25
|
+
*/
|
|
26
|
+
import { STOPWORDS } from "./context-file.js";
|
|
27
|
+
import { normalizeTerms } from "./investigate-extract.js";
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Claim splitting (extract-grammar terminators)
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
const STOPWORD_SET = new Set(STOPWORDS);
|
|
32
|
+
/**
|
|
33
|
+
* The verify-mode claim cap (PRD AC-1: > 8 sentence-claims is a
|
|
34
|
+
* fail-loud VALIDATION_ERROR, the R1 pipe-cap precedent — fail, never
|
|
35
|
+
* truncate). Exported so the command's error message names the cap
|
|
36
|
+
* from the single source of truth.
|
|
37
|
+
*/
|
|
38
|
+
export const MAX_VERIFY_CLAIMS = 8;
|
|
39
|
+
/**
|
|
40
|
+
* Split input into sentence windows on the extract grammar: `[.!?]`
|
|
41
|
+
* followed by a space (or end of input), plus newline boundaries.
|
|
42
|
+
* Pinned to lib/investigate-extract.ts splitWindows — a decimal like
|
|
43
|
+
* "3.14" has no space after the dot, so it never terminates.
|
|
44
|
+
*/
|
|
45
|
+
function splitWindows(content) {
|
|
46
|
+
const windows = [];
|
|
47
|
+
let start = 0;
|
|
48
|
+
for (let i = 0; i < content.length; i += 1) {
|
|
49
|
+
const ch = content.charAt(i);
|
|
50
|
+
const next = content.charAt(i + 1);
|
|
51
|
+
const isTerminator = (ch === "." || ch === "!" || ch === "?") &&
|
|
52
|
+
(i + 1 >= content.length || next === " ");
|
|
53
|
+
const isNewline = ch === "\n";
|
|
54
|
+
if (!isTerminator && !isNewline) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const end = i + 1; // terminator char owned by the window
|
|
58
|
+
windows.push({ start, end });
|
|
59
|
+
// Skip the horizontal-whitespace separator run; the next window
|
|
60
|
+
// starts at the first character that is neither space nor tab.
|
|
61
|
+
let cursor = end;
|
|
62
|
+
while (cursor < content.length) {
|
|
63
|
+
const sep = content.charAt(cursor);
|
|
64
|
+
if (sep !== " " && sep !== "\t") {
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
cursor += 1;
|
|
68
|
+
}
|
|
69
|
+
start = cursor;
|
|
70
|
+
i = cursor - 1; // loop increment lands on `cursor`
|
|
71
|
+
}
|
|
72
|
+
if (start < content.length) {
|
|
73
|
+
windows.push({ start, end: content.length });
|
|
74
|
+
}
|
|
75
|
+
return windows;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Split a statement into sentence-claims: extract-grammar windows,
|
|
79
|
+
* whitespace-only sentences dropped, quotes trimmed. Deterministic —
|
|
80
|
+
* the same statement yields the identical claim array byte-stably.
|
|
81
|
+
* `|` needs no handling: claims split on sentences, never pipes.
|
|
82
|
+
*/
|
|
83
|
+
export function splitClaims(statement) {
|
|
84
|
+
const claims = [];
|
|
85
|
+
for (const window of splitWindows(statement)) {
|
|
86
|
+
const claim = statement.slice(window.start, window.end).trim();
|
|
87
|
+
if (claim.length > 0) {
|
|
88
|
+
claims.push(claim);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return claims;
|
|
92
|
+
}
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
// Negation cues (frozen, versioned contract — PRD AC-5)
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
/**
|
|
97
|
+
* The negation-cue list behind `contradicted`. FROZEN and versioned:
|
|
98
|
+
* additions change verdicts and are release-visible (the parent-owned
|
|
99
|
+
* CHANGELOG bullet names them). Every cue is a LITERAL whole-word
|
|
100
|
+
* string (fix-round m1: the parenthesized inflection grammar is gone —
|
|
101
|
+
* variants spell out; a missing-absence family covers "without",
|
|
102
|
+
* "lacks", "untrue", "false", "absent", "rarely", "seldom").
|
|
103
|
+
*
|
|
104
|
+
* DISCLOSED as hint-grade: `contradicted` means "a term-matching
|
|
105
|
+
* passage carries a negation cue", NOT semantic contradiction —
|
|
106
|
+
* semantic judgment stays with the calling agent (agent-synthesis
|
|
107
|
+
* contract); the per-claim cue count exists for exactly that
|
|
108
|
+
* re-judgment.
|
|
109
|
+
*/
|
|
110
|
+
export const NEGATION_CUES = Object.freeze([
|
|
111
|
+
"not",
|
|
112
|
+
"no",
|
|
113
|
+
"never",
|
|
114
|
+
"none",
|
|
115
|
+
"cannot",
|
|
116
|
+
"can't",
|
|
117
|
+
"isn't",
|
|
118
|
+
"aren't",
|
|
119
|
+
"wasn't",
|
|
120
|
+
"weren't",
|
|
121
|
+
"doesn't",
|
|
122
|
+
"don't",
|
|
123
|
+
"didn't",
|
|
124
|
+
"won't",
|
|
125
|
+
"wouldn't",
|
|
126
|
+
"fail to",
|
|
127
|
+
"fails to",
|
|
128
|
+
"failed to",
|
|
129
|
+
"denies",
|
|
130
|
+
"denied",
|
|
131
|
+
"refutes",
|
|
132
|
+
"refuted",
|
|
133
|
+
"disputes",
|
|
134
|
+
"disputed",
|
|
135
|
+
"without",
|
|
136
|
+
"lacks",
|
|
137
|
+
"untrue",
|
|
138
|
+
"false",
|
|
139
|
+
"absent",
|
|
140
|
+
"rarely",
|
|
141
|
+
"seldom",
|
|
142
|
+
]);
|
|
143
|
+
const CUE_LITERALS = NEGATION_CUES;
|
|
144
|
+
function escapeRegExp(s) {
|
|
145
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
146
|
+
}
|
|
147
|
+
function containsWholeWord(content, term) {
|
|
148
|
+
const re = new RegExp(`(^|[^\\p{L}\\p{N}_])${escapeRegExp(term)}(?:[^\\p{L}\\p{N}_]|$)`, "iu");
|
|
149
|
+
return re.test(content);
|
|
150
|
+
}
|
|
151
|
+
/** Claim terms: lowercase tokens, stopword-filtered, normalizeTerms-dedupe. */
|
|
152
|
+
function claimTerms(claim) {
|
|
153
|
+
const tokens = claim
|
|
154
|
+
.toLowerCase()
|
|
155
|
+
.split(/[^a-z0-9']+/)
|
|
156
|
+
.filter((t) => t.length > 0 && !STOPWORD_SET.has(t));
|
|
157
|
+
return normalizeTerms(tokens);
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Match claims to evidence: per claim, every (source, passage) whose
|
|
161
|
+
* quote contains ≥ 1 claim term whole-word case-folded becomes an
|
|
162
|
+
* evidence pointer in first-encounter order; the cue scan runs over
|
|
163
|
+
* matching passages only; the verdict follows PRD-5 (corroborated =
|
|
164
|
+
* matches + 0 cues; contradicted = ≥ 1 cue-bearing match; unresolved
|
|
165
|
+
* = no matches).
|
|
166
|
+
*/
|
|
167
|
+
export function matchClaimsToEvidence({ statement, claims, sources }) {
|
|
168
|
+
const rows = claims.map((text) => {
|
|
169
|
+
const terms = claimTerms(text);
|
|
170
|
+
const evidence = [];
|
|
171
|
+
let cueBearers = 0;
|
|
172
|
+
sources.forEach((source, sourceIndex) => {
|
|
173
|
+
source.passages.forEach((passage, passageIndex) => {
|
|
174
|
+
const matched = terms.some((term) => containsWholeWord(passage.quote, term));
|
|
175
|
+
if (!matched)
|
|
176
|
+
return;
|
|
177
|
+
evidence.push({ sourceIndex, passageIndex });
|
|
178
|
+
const carriesCue = CUE_LITERALS.some((cue) => containsWholeWord(passage.quote.toLowerCase(), cue));
|
|
179
|
+
if (carriesCue)
|
|
180
|
+
cueBearers += 1;
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
const verdict = evidence.length === 0 ? "unresolved" : cueBearers > 0 ? "contradicted" : "corroborated";
|
|
184
|
+
return { text, verdict, negationCues: cueBearers, evidence };
|
|
185
|
+
});
|
|
186
|
+
return { statement, claims: rows };
|
|
187
|
+
}
|
|
188
|
+
//# sourceMappingURL=investigate-claims.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"investigate-claims.js","sourceRoot":"","sources":["../../src/lib/investigate-claims.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic passage extraction for the investigate pipeline
|
|
3
|
+
* (T3, docs/plans/investigate-pipeline DESIGN D4, PRD AC-5).
|
|
4
|
+
*
|
|
5
|
+
* Splits content into sentence windows with character-index tracking,
|
|
6
|
+
* keeps the windows containing >= 1 term as a whole word (case-folded),
|
|
7
|
+
* dedupes by exact quote equality, caps at 5 first-encounter passages.
|
|
8
|
+
*
|
|
9
|
+
* Boundary-char ownership: the terminator that ended a window
|
|
10
|
+
* (`.`/`!`/`?`/`\n`) belongs to the window; the separator run
|
|
11
|
+
* (spaces/tabs after a terminator) belongs to neither window. The
|
|
12
|
+
* round-trip pin — content.slice(...charRange) === quote for every
|
|
13
|
+
* passage — holds byte-exactly under this rule, and is invariant to
|
|
14
|
+
* it (any slicing convention preserves the pin).
|
|
15
|
+
*
|
|
16
|
+
* Byte-stable: no clocks, no randomness, no locale-dependent casing
|
|
17
|
+
* beyond toLowerCase. Self-contained: no imports.
|
|
18
|
+
*/
|
|
19
|
+
export interface Passage {
|
|
20
|
+
quote: string;
|
|
21
|
+
charRange: [number, number];
|
|
22
|
+
}
|
|
23
|
+
export interface ExtractPassagesArgs {
|
|
24
|
+
content: string;
|
|
25
|
+
terms: string[];
|
|
26
|
+
}
|
|
27
|
+
/** Normalize the term set: trim, case-fold, drop empties, dedupe. */
|
|
28
|
+
export declare function normalizeTerms(terms: string[]): string[];
|
|
29
|
+
/**
|
|
30
|
+
* Extract passages: sentence windows of `content` containing >= 1 of
|
|
31
|
+
* `terms` as a whole word (case-folded), deduped by exact quote,
|
|
32
|
+
* capped at 5 first-encounter. Empty term set -> [] (valid).
|
|
33
|
+
*/
|
|
34
|
+
export declare function extractPassages({ content, terms }: ExtractPassagesArgs): Passage[];
|
|
35
|
+
//# sourceMappingURL=investigate-extract.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"investigate-extract.d.ts","sourceRoot":"","sources":["../../src/lib/investigate-extract.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC7B;AAED,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAID,qEAAqE;AACrE,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CASxD;AA4DD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,mBAAmB,GAAG,OAAO,EAAE,CAqBlF"}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic passage extraction for the investigate pipeline
|
|
3
|
+
* (T3, docs/plans/investigate-pipeline DESIGN D4, PRD AC-5).
|
|
4
|
+
*
|
|
5
|
+
* Splits content into sentence windows with character-index tracking,
|
|
6
|
+
* keeps the windows containing >= 1 term as a whole word (case-folded),
|
|
7
|
+
* dedupes by exact quote equality, caps at 5 first-encounter passages.
|
|
8
|
+
*
|
|
9
|
+
* Boundary-char ownership: the terminator that ended a window
|
|
10
|
+
* (`.`/`!`/`?`/`\n`) belongs to the window; the separator run
|
|
11
|
+
* (spaces/tabs after a terminator) belongs to neither window. The
|
|
12
|
+
* round-trip pin — content.slice(...charRange) === quote for every
|
|
13
|
+
* passage — holds byte-exactly under this rule, and is invariant to
|
|
14
|
+
* it (any slicing convention preserves the pin).
|
|
15
|
+
*
|
|
16
|
+
* Byte-stable: no clocks, no randomness, no locale-dependent casing
|
|
17
|
+
* beyond toLowerCase. Self-contained: no imports.
|
|
18
|
+
*/
|
|
19
|
+
const MAX_PASSAGES = 5;
|
|
20
|
+
/** Normalize the term set: trim, case-fold, drop empties, dedupe. */
|
|
21
|
+
export function normalizeTerms(terms) {
|
|
22
|
+
const seen = new Set();
|
|
23
|
+
for (const term of terms) {
|
|
24
|
+
const normalized = term.trim().toLowerCase();
|
|
25
|
+
if (normalized.length > 0) {
|
|
26
|
+
seen.add(normalized);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return [...seen];
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Split content into sentence windows on `[.!?]` terminators followed
|
|
33
|
+
* by a space (the literal `[.!?] +` grammar), plus newline
|
|
34
|
+
* boundaries, tracking exact offsets into the original content.
|
|
35
|
+
* Newlines terminate but never separate: "here.\n" ends at the `\n`.
|
|
36
|
+
*/
|
|
37
|
+
function splitWindows(content) {
|
|
38
|
+
const windows = [];
|
|
39
|
+
let start = 0;
|
|
40
|
+
for (let i = 0; i < content.length; i += 1) {
|
|
41
|
+
const ch = content.charAt(i);
|
|
42
|
+
const next = content.charAt(i + 1);
|
|
43
|
+
const isTerminator = (ch === "." || ch === "!" || ch === "?") &&
|
|
44
|
+
(i + 1 >= content.length || next === " ");
|
|
45
|
+
const isNewline = ch === "\n";
|
|
46
|
+
if (!isTerminator && !isNewline) {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const end = i + 1; // terminator char owned by the window
|
|
50
|
+
windows.push({ start, end });
|
|
51
|
+
// Skip the horizontal-whitespace separator run; the next window
|
|
52
|
+
// starts at the first character that is neither space nor tab.
|
|
53
|
+
let cursor = end;
|
|
54
|
+
while (cursor < content.length) {
|
|
55
|
+
const sep = content.charAt(cursor);
|
|
56
|
+
if (sep !== " " && sep !== "\t") {
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
cursor += 1;
|
|
60
|
+
}
|
|
61
|
+
start = cursor;
|
|
62
|
+
i = cursor - 1; // loop increment lands on `cursor`
|
|
63
|
+
}
|
|
64
|
+
if (start < content.length) {
|
|
65
|
+
windows.push({ start, end: content.length });
|
|
66
|
+
}
|
|
67
|
+
return windows;
|
|
68
|
+
}
|
|
69
|
+
function containsWholeWord(content, term) {
|
|
70
|
+
const re = new RegExp(`(^|[^\\p{L}\\p{N}_])${escapeRegExp(term)}(?:[^\\p{L}\\p{N}_]|$)`, "iu");
|
|
71
|
+
return re.test(content);
|
|
72
|
+
}
|
|
73
|
+
function escapeRegExp(s) {
|
|
74
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Extract passages: sentence windows of `content` containing >= 1 of
|
|
78
|
+
* `terms` as a whole word (case-folded), deduped by exact quote,
|
|
79
|
+
* capped at 5 first-encounter. Empty term set -> [] (valid).
|
|
80
|
+
*/
|
|
81
|
+
export function extractPassages({ content, terms }) {
|
|
82
|
+
const normalized = normalizeTerms(terms);
|
|
83
|
+
if (normalized.length === 0) {
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
const seenQuotes = new Set();
|
|
87
|
+
const passages = [];
|
|
88
|
+
for (const window of splitWindows(content)) {
|
|
89
|
+
const quote = content.slice(window.start, window.end);
|
|
90
|
+
if (normalized.some((term) => containsWholeWord(quote, term)) &&
|
|
91
|
+
!seenQuotes.has(quote)) {
|
|
92
|
+
seenQuotes.add(quote);
|
|
93
|
+
passages.push({ quote, charRange: [window.start, window.end] });
|
|
94
|
+
if (passages.length >= MAX_PASSAGES) {
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return passages;
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=investigate-extract.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"investigate-extract.js","sourceRoot":"","sources":["../../src/lib/investigate-extract.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Three-tier deterministic sub-query planner for `investigate`
|
|
3
|
+
* (investigate-pipeline lane, Ticket T2; DESIGN.md D2, ADR-0013).
|
|
4
|
+
*
|
|
5
|
+
* Tier resolution order:
|
|
6
|
+
* 1. explicit — the query contains an unescaped `|`: split on the
|
|
7
|
+
* SAME grammar `search --merge` uses; wins over `--context` with
|
|
8
|
+
* a notice stating the override.
|
|
9
|
+
* 2. context — a `--context` file is present: `deriveSubQueries`
|
|
10
|
+
* over the parsed text, verbatim (no original prepend — context
|
|
11
|
+
* mode owns the grid, mirroring `search --context`). An empty
|
|
12
|
+
* derivation degrades to the original query.
|
|
13
|
+
* 3. template — transforms of the bare question (see TEMPLATE
|
|
14
|
+
* order below).
|
|
15
|
+
*
|
|
16
|
+
* Byte-stable everywhere: no clocks, no randomness, no
|
|
17
|
+
* locale-dependent casing beyond `toLowerCase`. The unit has no
|
|
18
|
+
* filesystem access — `loadContextText` is dependency-injected;
|
|
19
|
+
* production wiring lives in the orchestrator command.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* PR #264 R1 (owner-approved): explicit pipe plans are capped at 8
|
|
23
|
+
* sub-queries — the context tier's MAX_SUBQUERIES precedent. Exceeding
|
|
24
|
+
* the cap fails loud with VALIDATION_ERROR (silent truncation rejected:
|
|
25
|
+
* a truncated plan would silently under-bill the user's intent).
|
|
26
|
+
*/
|
|
27
|
+
export declare const MAX_EXPLICIT_SUBQUERIES = 8;
|
|
28
|
+
export type PlannerTier = "explicit" | "context" | "template";
|
|
29
|
+
export interface PlanSubQueriesOptions {
|
|
30
|
+
readonly query: string;
|
|
31
|
+
/** `--context` file path; presence selects the context tier. */
|
|
32
|
+
readonly contextFile?: string;
|
|
33
|
+
}
|
|
34
|
+
export interface PlanSubQueriesDeps {
|
|
35
|
+
/**
|
|
36
|
+
* Injected context reader (production: the context-file io
|
|
37
|
+
* adapter). The unit never touches the filesystem itself.
|
|
38
|
+
*/
|
|
39
|
+
loadContextText(filePath: string): Promise<string>;
|
|
40
|
+
}
|
|
41
|
+
export interface SubQueryPlan {
|
|
42
|
+
readonly subQueries: readonly string[];
|
|
43
|
+
readonly tier: PlannerTier;
|
|
44
|
+
readonly notice?: string;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Key-terms join for the template tier: lowercase the question,
|
|
48
|
+
* tokenize on non-alphanumerics, drop stopwords and tokens outside the
|
|
49
|
+
* context-file term bounds, preserve order, dedupe, join with spaces.
|
|
50
|
+
* Only `toLowerCase` is applied (byte-stable, no locale).
|
|
51
|
+
*/
|
|
52
|
+
export declare function deriveTemplateTopic(query: string): string;
|
|
53
|
+
/**
|
|
54
|
+
* Resolve the sub-query plan for an investigation. Throws only on the
|
|
55
|
+
* explicit-tier fail-loud cases (all fragments empty; > 8 fragments).
|
|
56
|
+
*/
|
|
57
|
+
export declare function planSubQueries(options: PlanSubQueriesOptions, deps: PlanSubQueriesDeps): Promise<SubQueryPlan>;
|
|
58
|
+
//# sourceMappingURL=investigate-planner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"investigate-planner.d.ts","sourceRoot":"","sources":["../../src/lib/investigate-planner.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAKH;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,IAAI,CAAC;AAyBzC,MAAM,MAAM,WAAW,GAAG,UAAU,GAAG,SAAS,GAAG,UAAU,CAAC;AAE9D,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,gEAAgE;IAChE,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,WAAW,kBAAkB;IACjC;;;OAGG;IACH,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CACpD;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,UAAU,EAAE,SAAS,MAAM,EAAE,CAAC;IACvC,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAC3B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAczD;AAwBD;;;GAGG;AACH,wBAAsB,cAAc,CAClC,OAAO,EAAE,qBAAqB,EAC9B,IAAI,EAAE,kBAAkB,GACvB,OAAO,CAAC,YAAY,CAAC,CA6BvB"}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Three-tier deterministic sub-query planner for `investigate`
|
|
3
|
+
* (investigate-pipeline lane, Ticket T2; DESIGN.md D2, ADR-0013).
|
|
4
|
+
*
|
|
5
|
+
* Tier resolution order:
|
|
6
|
+
* 1. explicit — the query contains an unescaped `|`: split on the
|
|
7
|
+
* SAME grammar `search --merge` uses; wins over `--context` with
|
|
8
|
+
* a notice stating the override.
|
|
9
|
+
* 2. context — a `--context` file is present: `deriveSubQueries`
|
|
10
|
+
* over the parsed text, verbatim (no original prepend — context
|
|
11
|
+
* mode owns the grid, mirroring `search --context`). An empty
|
|
12
|
+
* derivation degrades to the original query.
|
|
13
|
+
* 3. template — transforms of the bare question (see TEMPLATE
|
|
14
|
+
* order below).
|
|
15
|
+
*
|
|
16
|
+
* Byte-stable everywhere: no clocks, no randomness, no
|
|
17
|
+
* locale-dependent casing beyond `toLowerCase`. The unit has no
|
|
18
|
+
* filesystem access — `loadContextText` is dependency-injected;
|
|
19
|
+
* production wiring lives in the orchestrator command.
|
|
20
|
+
*/
|
|
21
|
+
import { ValidationError } from "./errors.js";
|
|
22
|
+
import { STOPWORDS, parseContextText } from "./context-file.js";
|
|
23
|
+
/**
|
|
24
|
+
* PR #264 R1 (owner-approved): explicit pipe plans are capped at 8
|
|
25
|
+
* sub-queries — the context tier's MAX_SUBQUERIES precedent. Exceeding
|
|
26
|
+
* the cap fails loud with VALIDATION_ERROR (silent truncation rejected:
|
|
27
|
+
* a truncated plan would silently under-bill the user's intent).
|
|
28
|
+
*/
|
|
29
|
+
export const MAX_EXPLICIT_SUBQUERIES = 8;
|
|
30
|
+
/**
|
|
31
|
+
* Pinned to `src/commands/search.ts` `splitMergeSubQueries` — the
|
|
32
|
+
* same split grammar the `--merge` flag defines: split on unescaped
|
|
33
|
+
* `|` (a literal pipe is escaped as `\|`), unescape, trim, drop
|
|
34
|
+
* empty fragments. Deliberately duplicated as a local constant: the
|
|
35
|
+
* original is module-private there and this lane's file allowlist
|
|
36
|
+
* keeps `search.ts` read-only (orchestrator ruling, T2 pickup).
|
|
37
|
+
*/
|
|
38
|
+
const MERGE_SPLIT_PATTERN = /(?<!\\)\|/;
|
|
39
|
+
const MERGE_UNESCAPE = /\\\|/g;
|
|
40
|
+
/**
|
|
41
|
+
* Pinned to `src/lib/context-file.ts` `MIN_TERM_CHARS`/`MAX_TERM_CHARS`
|
|
42
|
+
* (module-private there; allowlist keeps the file read-only — same
|
|
43
|
+
* ruling as the splitter). Keep in sync with that module's D2.3 bounds.
|
|
44
|
+
*/
|
|
45
|
+
const MIN_TERM_CHARS = 4;
|
|
46
|
+
const MAX_TERM_CHARS = 40;
|
|
47
|
+
const STOPWORD_SET = new Set(STOPWORDS);
|
|
48
|
+
const TEMPLATE_SUFFIXES = ["overview", "evidence", "criticism"];
|
|
49
|
+
/**
|
|
50
|
+
* Key-terms join for the template tier: lowercase the question,
|
|
51
|
+
* tokenize on non-alphanumerics, drop stopwords and tokens outside the
|
|
52
|
+
* context-file term bounds, preserve order, dedupe, join with spaces.
|
|
53
|
+
* Only `toLowerCase` is applied (byte-stable, no locale).
|
|
54
|
+
*/
|
|
55
|
+
export function deriveTemplateTopic(query) {
|
|
56
|
+
const terms = [];
|
|
57
|
+
const seen = new Set();
|
|
58
|
+
for (const token of query.toLowerCase().split(/[^a-z0-9]+/)) {
|
|
59
|
+
if (token.length < MIN_TERM_CHARS || token.length > MAX_TERM_CHARS) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (STOPWORD_SET.has(token) || seen.has(token)) {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
seen.add(token);
|
|
66
|
+
terms.push(token);
|
|
67
|
+
}
|
|
68
|
+
return terms.join(" ");
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Explicit tier split — see the grammar pin above. Same fail-loud
|
|
72
|
+
* contract as `search --merge`: no non-empty fragments is an error.
|
|
73
|
+
* R1: > 8 fragments is VALIDATION_ERROR (never silent truncation).
|
|
74
|
+
*/
|
|
75
|
+
function splitExplicit(query) {
|
|
76
|
+
const subQueries = query
|
|
77
|
+
.split(MERGE_SPLIT_PATTERN)
|
|
78
|
+
.map((q) => q.replace(MERGE_UNESCAPE, "|").trim())
|
|
79
|
+
.filter((q) => q.length > 0);
|
|
80
|
+
if (subQueries.length === 0) {
|
|
81
|
+
throw new Error("--merge requires at least one non-empty query (split with '|')");
|
|
82
|
+
}
|
|
83
|
+
if (subQueries.length > MAX_EXPLICIT_SUBQUERIES) {
|
|
84
|
+
throw new ValidationError(`Explicit pipe plan exceeds the ${MAX_EXPLICIT_SUBQUERIES}-sub-query cap (${subQueries.length} fragments).`, "Split fewer sub-queries with '|', or move the investigation into a --context file (the context tier derives up to 8).");
|
|
85
|
+
}
|
|
86
|
+
return subQueries;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Resolve the sub-query plan for an investigation. Throws only on the
|
|
90
|
+
* explicit-tier fail-loud cases (all fragments empty; > 8 fragments).
|
|
91
|
+
*/
|
|
92
|
+
export async function planSubQueries(options, deps) {
|
|
93
|
+
const hasPipes = MERGE_SPLIT_PATTERN.test(options.query);
|
|
94
|
+
if (hasPipes) {
|
|
95
|
+
return {
|
|
96
|
+
subQueries: splitExplicit(options.query),
|
|
97
|
+
tier: "explicit",
|
|
98
|
+
notice: "--context ignored: explicit pipe split takes precedence over the context file",
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
if (options.contextFile !== undefined) {
|
|
102
|
+
const text = await deps.loadContextText(options.contextFile);
|
|
103
|
+
const derived = [...parseContextText(text).subQueries];
|
|
104
|
+
// Orchestrator ruling (T2 pickup): search's zero-derivation
|
|
105
|
+
// fallback precedent — an empty derivation degrades to the
|
|
106
|
+
// original query rather than an empty grid.
|
|
107
|
+
const subQueries = derived.length > 0 ? derived : [options.query];
|
|
108
|
+
return { subQueries, tier: "context" };
|
|
109
|
+
}
|
|
110
|
+
const topic = deriveTemplateTopic(options.query);
|
|
111
|
+
const subQueries = [options.query];
|
|
112
|
+
if (topic.length > 0) {
|
|
113
|
+
subQueries.push(topic);
|
|
114
|
+
for (const suffix of TEMPLATE_SUFFIXES) {
|
|
115
|
+
subQueries.push(`${topic} ${suffix}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return { subQueries: dedupe(subQueries), tier: "template" };
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Exact-equal dedupe on trimmed, case-sensitive members, preserving
|
|
122
|
+
* first-occurrence order, capped at 5 (the template tier emits at
|
|
123
|
+
* most 5 candidates, so the cap is a hard upper bound).
|
|
124
|
+
*/
|
|
125
|
+
function dedupe(candidates) {
|
|
126
|
+
const seen = new Set();
|
|
127
|
+
const result = [];
|
|
128
|
+
for (const candidate of candidates) {
|
|
129
|
+
const key = candidate.trim();
|
|
130
|
+
if (key.length === 0 || seen.has(key)) {
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
seen.add(key);
|
|
134
|
+
result.push(key);
|
|
135
|
+
if (result.length === 5) {
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return result;
|
|
140
|
+
}
|
|
141
|
+
//# sourceMappingURL=investigate-planner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"investigate-planner.js","sourceRoot":"","sources":["../../src/lib/investigate-planner.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/providers/registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAwBH,OAAO,KAAK,EAAE,kBAAkB,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAMjE;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,6BAA6B,EAAE,SAAS,kBAAkB,
|
|
1
|
+
{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/providers/registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAwBH,OAAO,KAAK,EAAE,kBAAkB,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAMjE;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,6BAA6B,EAAE,SAAS,kBAAkB,EA4BtE,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,qBAAqB,CACnC,EAAE,EAAE,UAAU,EACd,WAAW,GAAE,SAAS,kBAAkB,EAAkC,GACzE,kBAAkB,CAEpB;AAED;;;GAGG;AACH,wBAAgB,gCAAgC,CAC9C,GAAG,EAAE,MAAM,CAAC,UAAU,EACtB,WAAW,GAAE,SAAS,kBAAkB,EAAkC,GACzE,SAAS,kBAAkB,EAAE,CAE/B"}
|