/** * Find the commitments and questions an agent left open in a reply to the * owner (specs/2026-10-01-reply-open-items.md §1). Pure, rules only. * * Tuned to miss rather than invent: a missed promise costs one follow-up the * owner has to ask for himself; an invented question puts a pointless card in * front of him. So a sentence only counts when it clearly says * * - **commitment**: a first-person future deliverable for the owner * ("明天中午给我,我核过以后挑最有用的几条发你", "I'll send the draft tonight"); * A plan of concrete next steps with no deliverable named counts too * ("让它们跑完,对比后看差多少", "then I'll do step 2"; crewly#1386); * - **question**: a direct question that asks the owner to decide or confirm * ("这个读法,你同意吗?", "Should I keep the appendix?") — and only the * message's FINAL question (nothing but a fallback or a sign-off after it). * * Skipped on purpose: past tense ("已经发你了", "attached below"), conditional * offers ("要的话我再…", "if you want, I'll…"), quoted text, headings, * questions to a colleague (an @-mention or a teammate's name up front), * questions in a list or written for a third party (interview / survey * questions), questions in the middle of a message, and rhetorical questions * that the agent answers itself. * * @module services/open-items/open-item-extractor */ /** A commitment found in a reply. */ export interface ExtractedCommitment { type: 'commitment'; /** The sentence (clipped) */ text: string; /** When it is due */ due: Date; /** `text` = a time was said; `default` = none was, so +24 h */ dueSource: 'text' | 'default'; /** Conditional on the owner ("你点头后…"): no due time until they say yes */ waitsOnOwner?: boolean; } /** A question to the owner found in a reply. */ export interface ExtractedQuestion { type: 'question'; /** The question sentence (clipped, one line) */ text: string; /** The sentence after it, when it states a fallback ("不同意的话我就删掉") */ fallback?: string; } /** Everything found in one reply. */ export interface ExtractedOpenItems { commitments: ExtractedCommitment[]; questions: ExtractedQuestion[]; } /** Options for {@link extractOpenItems}. */ export interface ExtractOptions { /** When the reply was posted (due times are relative to it) */ now: Date; /** Display names of the agent's colleagues (a question addressed to one is not for the owner) */ colleagueNames?: readonly string[]; /** The owner's Slack user id: an @-mention of anyone else makes a sentence not for the owner */ ownerSlackUserId?: string; /** Due time of a commitment that names none (ms after `now`); default {@link OPEN_ITEMS_CONSTANTS.DEFAULT_DUE_MS} */ defaultDueMs?: number; } /** One sentence with the line it came from. */ interface Sentence { text: string; /** Index of the sentence within its line */ index: number; /** All sentences of the same line */ line: string[]; /** The line was a list item (bullet or numbered) */ listItem?: boolean; /** The prose line before this sentence's line (blank lines skipped), if any */ prevLine?: string; /** Paragraph number (blank lines separate paragraphs) */ para?: number; /** The paragraph is introduced as a script / draft / transcript the agent is delivering ("口播脚本:", "Script:") */ scriptFrame?: boolean; } /** * Split a reply into sentences, line by line. * * @param text - Reply text * @returns Sentences */ export declare function splitSentences(text: string): Sentence[]; /** * Whether a commitment sentence waits on the owner's say-so. * * @param s - A sentence {@link isCommitment} accepted * @returns True when it is conditional on the owner ("你点头后…") */ export declare function waitsOnOwner(s: string): boolean; /** * Whether a sentence is a commitment to the owner. * * @param s - Sentence * @returns True for a first-person future deliverable */ export declare function isCommitment(s: string): boolean; /** * Whether a sentence commits to concrete next steps with no deliverable named * ("让它们跑完,对比后看差多少", "这批之后到第 2 步结束", "then I'll do step 2"). * {@link isCommitment} needs a deliverable for the owner ("发你", "send you"), * so a plan of next steps was never tracked and nobody came back to it when * the owner moved on (crewly#1386, Pia 2026-10-18). * * Conservative on purpose — it needs all of: * - the agent as the one acting: a first-person subject doing a step * ("我对比", "I'll run"), or (Chinese only) a sentence that opens on the * sequence itself ("让它们跑完…", "这批之后…", "接下来…"); * - a sequence: one step after another (然后 / 之后 / X 完后 / 第 N 步, * then / next / after that / once they finish / step N); * - a concrete action (对比, 跑, 复刻, compare, run, do step 2 …); * - no question, condition, habit, caveat, opinion or hedge, nothing addressed * to the owner as the actor (你 / you), and (Chinese) the step itself not in * the past ("三条已经提交," is dropped, the rest is judged). * * @param s - Sentence * @returns True for a first-person future plan of concrete next steps * * @example * isNextStepCommitment('三条已经提交,让它们跑完,对比后看差多少。'); // true * isNextStepCommitment('我再看看。'); // false — no sequence, no concrete step */ export declare function isNextStepCommitment(s: string): boolean; /** * The number a numeral token names: ASCII digits ("1.5"), Chinese numerals up * to 99 ("两", "十五", "二十", "三十五"), "半" (0.5), or an approximate pair of * consecutive numerals ("一两", "两三", "三四" → the upper one). * * @param token - Numeral token * @returns The number, or null when it is not one */ export declare function numeralValue(token: string): number | null; /** * Milliseconds of a duration named in the text: "2 小时", "两小时", "三十分钟", * "一个半小时", "半天", "一天", "1.5 到 2 小时" (a range → its upper bound), * "an hour", "a couple of hours", "half a day". "左右", "大约", "约", "大概" * do not change the number. * * @param s - Sentence (any case) * @returns Milliseconds, or null when no duration is named * * @example * durationMs('预计一小时左右') // → 3600000 * durationMs('40 到 60 分钟') // → 3600000 */ export declare function durationMs(s: string): number | null; /** * When a commitment is due. * * Rules (local time): an explicit date (10/7, 10月7日, 2026-10-07) wins over everything; an explicit day/part of day wins ("明天中午" → tomorrow * 12:00, "tonight" → today 21:00, "by Friday" → Friday 12:00); "tomorrow" * alone → tomorrow {@link OPEN_ITEMS_CONSTANTS.DEFAULT_DUE_HOUR_LOCAL}:00; * "in 2 hours" / "两小时" / "一两个小时" / "40 到 60 分钟" → + that long (the * upper bound of a range, see {@link durationMs}); nothing → +`defaultMs` * ({@link OPEN_ITEMS_CONSTANTS.DEFAULT_DUE_MS} unless the caller says otherwise). * * @param s - Sentence * @param now - When it was said * @param defaultMs - Due this long after `now` when the text names no time * @returns Due time and whether the text named it */ export declare function parseDue(s: string, now: Date, defaultMs?: number): { due: Date; source: 'text' | 'default'; }; /** * The due time an estimate sentence states, for a commitment sentence that * names none: a duration ("预计一小时左右", "大约 2 小时", "ETA 30 min") or a * time framed as an estimate ({@link ESTIMATE_CUE}: "预计明天中午能好"), in a * sentence that is not itself a commitment or a question — "预计一小时左右。" * after "改完发你。" dates the promise; "明天中午给你另一份。" is its own * promise and leaves it alone. * * @param s - The neighbouring sentence (undefined when there is none) * @param now - When it was said * @returns The due time, or null */ export declare function estimateDue(s: string | undefined, now: Date): Date | null; /** * Whether a sentence (with its neighbours) is a question for the owner. * * @param s - The sentence * @param next - The sentence after it on the same line, if any * @param opts - Extraction options * @returns True for a decision/confirmation question to the owner */ export declare function isOwnerQuestion(s: string, next: string | undefined, opts: ExtractOptions): boolean; /** * Find open items in an agent's reply to the owner. * * @param text - The reply * @param opts - Clock, colleague names, owner id * @returns Commitments and questions (each at most * {@link OPEN_ITEMS_CONSTANTS.MAX_ITEMS_PER_REPLY}) * * @example * extractOpenItems('明天中午给我,我核过以后挑最有用的几条发你。', { now }) * // → { commitments: [{ text: '明天中午给我,我核过以后挑最有用的几条发你。', due: , … }], questions: [] } */ export declare function extractOpenItems(text: string, opts: ExtractOptions): ExtractedOpenItems; /** A yes/no decision asked of the owner in prose. */ export interface ExtractedOwnerQuestion { type: 'owner_question'; /** The question sentence (clipped, one line) */ text: string; /** When an answer is expected ({@link OPEN_ITEMS_CONSTANTS.OWNER_QUESTION_DUE_MS} after it was asked) */ due: Date; } /** * Whether a sentence asks the owner a yes/no decision about the work * ("Should I close PR #1138?", "要不要合并?"). Conservative on purpose: a * question mark, a yes/no ask shape AND a decision verb, not quoted, not * rhetorical, not answered by the next sentence, not for a colleague. * * @param s - Sentence * @param next - The sentence after it on the same line, if any * @param opts - Extraction options * @returns True for an owner decision */ export declare function isOwnerDecisionQuestion(s: string, next: string | undefined, opts: ExtractOptions): boolean; /** * Every yes/no decision an agent asked the owner in a message, anywhere in * it (not only the final question {@link extractOpenItems} cards). Each goes * on file with a due time so the answer — or the silence — is recorded and * followed up (crewly#1369: "Ella asked Steve whether to close PR #1138" left * no record, and nobody knew what he answered). * * @param text - The agent's message * @param opts - Clock, colleague names, owner id * @returns At most {@link OPEN_ITEMS_CONSTANTS.MAX_ITEMS_PER_REPLY} questions, in order * * @example * extractOwnerDecisionQuestions('PR #1138 is still open. Should I close it? Tests pass.', { now }) * // → [{ type: 'owner_question', text: 'Should I close it?', due: }] */ export declare function extractOwnerDecisionQuestions(text: string, opts: ExtractOptions): ExtractedOwnerQuestion[]; export {}; //# sourceMappingURL=open-item-extractor.d.ts.map