src / prepend / TimeDependencyDetector.ts
src / prepend / TimeDependencyDetector.ts
function escapeRegExp(str: string): string {
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function capitalizeWords(str: string): string {
return str
.split(" ")
.map(w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
.join(" ");
}
export interface TimePatternRule {
readonly description: string;
readonly regex: RegExp;
}
export interface TimeRuleGroupConfig {
readonly category: string;
/** Standalone phrases/words matched with word boundaries (e.g., "today", "this afternoon", "stock price"). */
readonly terms?: readonly string[];
/** Pattern-based rules for structural queries (e.g., relative offsets like "3 days ago" or role patterns). */
readonly patterns?: readonly TimePatternRule[];
}
export class TimeRuleGroup {
readonly category: string;
readonly terms: readonly string[];
readonly patterns: readonly TimePatternRule[];
private readonly compiledTermRegexes: readonly { term: string; regex: RegExp }[];
constructor(config: TimeRuleGroupConfig) {
this.category = config.category;
this.terms = config.terms ?? [];
this.patterns = config.patterns ?? [];
this.compiledTermRegexes = this.terms.map(term => ({
term,
regex: new RegExp(`\\b${escapeRegExp(term.trim())}\\b`, "i"),
}));
}
public matches(text: string): boolean {
return (
this.compiledTermRegexes.some(tr => tr.regex.test(text)) ||
this.patterns.some(p => p.regex.test(text))
);
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Dynamic Year Detection Helper
// ─────────────────────────────────────────────────────────────────────────────
/**
* Dynamically builds a regex alternation pattern matching the current 4-digit year
* and any future 4-digit years (e.g., 2026 -> 202[6-9]|20[3-9]\d|2[1-9]\d{2}|[3-9]\d{3}).
*/
export function buildCurrentAndFutureYearPattern(currentYear: number = new Date().getFullYear()): string {
const yStr = String(currentYear);
if (yStr.length !== 4) {
return `${currentYear}|202[6-9]|20[3-9]\\d|2[1-9]\\d{2}|[3-9]\\d{3}`;
}
const d0 = parseInt(yStr[0], 10);
const d1 = parseInt(yStr[1], 10);
const d2 = parseInt(yStr[2], 10);
const d3 = parseInt(yStr[3], 10);
const branches: string[] = [];
// 1. Same d0, d1, d2, and units from d3 to 9
if (d3 === 9) {
branches.push(`${d0}${d1}${d2}9`);
} else {
branches.push(`${d0}${d1}${d2}[${d3}-9]`);
}
// 2. Same d0, d1, and tens from (d2 + 1) to 9
if (d2 < 9) {
const nextTens = d2 + 1;
if (nextTens === 9) {
branches.push(`${d0}${d1}9\\d`);
} else {
branches.push(`${d0}${d1}[${nextTens}-9]\\d`);
}
}
// 3. Same d0, and hundreds from (d1 + 1) to 9
if (d1 < 9) {
const nextHundreds = d1 + 1;
if (nextHundreds === 9) {
branches.push(`${d0}9\\d{2}`);
} else {
branches.push(`${d0}[${nextHundreds}-9]\\d{2}`);
}
}
// 4. Thousands from (d0 + 1) to 9
if (d0 < 9) {
const nextThousands = d0 + 1;
if (nextThousands === 9) {
branches.push(`9\\d{3}`);
} else {
branches.push(`[${nextThousands}-9]\\d{3}`);
}
}
return branches.join("|");
}
export const currentAndFutureYearPattern = buildCurrentAndFutureYearPattern();
// ─────────────────────────────────────────────────────────────────────────────
// 1. Relative Time Group
// ─────────────────────────────────────────────────────────────────────────────
const relativeDayParts = "(?:morning|afternoon|evening|night|noon|midday|forenoon)";
const relativePeriods = "(?:week|weekend|month|quarter|year|semester|term|decade|century)";
const relativeSeasons = "(?:season|summer|winter|spring|autumn|fall|monsoon)";
export const relativeTimeRuleGroup = new TimeRuleGroup({
category: "Relative Time",
terms: [
"today",
"tomorrow",
"yesterday",
"tonight",
"right now",
"overnight",
"local time",
"time zone",
"timezone",
"what day is it",
],
patterns: [
{
description: "this morning / afternoon / evening / night / noon / midday / forenoon",
regex: new RegExp(`\\bthis\\s+${relativeDayParts}\\b`, "i"),
},
{
description: "this|next|last week / weekend / month / quarter / year / semester / term / decade / century",
regex: new RegExp(`\\b(?:this|next|last)\\s+${relativePeriods}\\b`, "i"),
},
{
description: "this|next|last season / summer / winter / spring / autumn / fall / monsoon",
regex: new RegExp(`\\b(?:this|next|last)\\s+${relativeSeasons}\\b`, "i"),
},
{
description: "so far today / tonight / this <day part / period / season>",
regex: new RegExp(
`\\bso\\s+far\\s+(?:today|tonight|this\\s+(?:${relativeDayParts}|${relativePeriods}|${relativeSeasons}))\\b`,
"i"
),
},
{
description: "as of today / tonight / yesterday / last night / this|last <day part / period / season>",
regex: new RegExp(
`\\bas\\s+of\\s+(?:today|tonight|yesterday|last\\s+night|this\\s+${relativeDayParts}|(?:this|last)\\s+(?:${relativePeriods}|${relativeSeasons}))\\b`,
"i"
),
},
{
description: "this|next|last <weekday>",
regex: /\b(this|next|last)\s+(monday|tuesday|wednesday|thursday|friday|saturday|sunday)\b/i,
},
{
description: "N days ago / a few days ago",
regex: /\b(?:\d+|a\s+few|a\s+couple(?:\s+of)?|several|a|one)\s+(?:sec|second|min|minute|hour|day|week|month|year)s?\s+ago\b/i,
},
{
description: "what time is it",
regex: /\bwhat(?:'s|s|\s+is)?\s+(?:the\s+)?(?:time|date|day|year|month)\b/i,
},
{
description: "temporal year anchor (in/for/during/throughout/as of/so far in/since/by/from <current/future year>)",
regex: new RegExp(
`\\b(?:so\\s+far\\s+in|as\\s+of|throughout|during|in|for|since|by|from)\\s+(?:${currentAndFutureYearPattern})\\b`,
"i"
),
},
{
description: "as of/in/for/since/during/by/from/throughout <month> <current/future year>",
regex: new RegExp(
`\\b(?:as\\s+of|in|for|since|during|by|from|throughout)\\s+(?:january|february|march|april|may|june|july|august|september|october|november|december|jan|feb|mar|apr|jun|jul|aug|sep|sept|oct|nov|dec)\\s+(?:\\d{1,2},?\\s+)?(?:${currentAndFutureYearPattern})\\b`,
"i"
),
},
{
description: "rolling past duration window (in/over/during/within/for/throughout the last/past N units)",
regex: /\b(?:(?:in|over|during|within|for|throughout)\s+(?:the\s+)?)?(?:last|past|previous)\s+(?:\d+|a|an|few|couple(?:\s+of)?|several)\s+(?:sec|second|min|minute|hour|day|week|month|year)s?\b|\b(?:in|over|during|within|for|throughout)\s+recent\s+(?:days|weeks|months|years|hours)\b/i,
},
{
description: "relative future duration (in/within N units or over/during/for/throughout next N units)",
regex: /\b(?:in|within)\s+(?:(?:the\s+)?(?:next|coming)\s+)?(?:\d+|two|three|four|five|six|seven|eight|nine|ten|a|an|few|couple(?:\s+of)?|several)\s+(?:sec|second|min|minute|hour|day|week|month|year)s?\b|\b(?:over|during|for|throughout)\s+(?:the\s+)?(?:next|coming)\s+(?:\d+|two|three|four|five|six|seven|eight|nine|ten|a|an|few|couple(?:\s+of)?|several)\s+(?:sec|second|min|minute|hour|day|week|month|year)s?\b/i,
},
{
description: "how long / much longer until/before",
regex: /\bhow\s+(?:long|much\s+longer)\s+(?:until|till|before)\b/i,
},
{
description: "how much time left / until",
regex: /\bhow\s+much\s+time\s+(?:is\s+left|until|till|remaining|left)\b/i,
},
],
});
// ─────────────────────────────────────────────────────────────────────────────
// 2. Recency Group (Includes Version & Releases)
// ─────────────────────────────────────────────────────────────────────────────
const inquiryFrames = "(?:(?:is|are|was)\\s+there|do\\s+you\\s+have|do\\s+we\\s+have|got|have\\s+you\\s+heard)";
const allDeterminers = "(?:any|some|more|much|the|a|an)";
const standaloneDeterminers = "(?:any|some|more|much)";
const recencyAdjectives = "(?:new|latest|recent|most\\s+recent|fresh|breaking|upcoming)";
const qualityModifiers = "(?:good|bad|positive|negative|exciting|promising|major|important|significant|big|huge|massive|critical|key|main)";
const releaseModifiers = "(?:major|big|significant|main|stable|minor)";
const mannerAdverbs = "(?:officially|widely|actively|fully|currently|generally|publicly|commercially)";
const strongRecencyNouns = "(?:news|updates?|developments?|changes?|reports?)";
const weakInformationalNouns = "(?:details?|coverage|info|information)";
const topicalPrepositions = "(?:on|about|of|with|for|regarding|in|around|to)";
const releaseEntities = "(?:season|episode|release|version|model|edition|generation|update)";
const releaseInquiryFrames = "(?:when(?:'s|s|\\s+is|\\s+will|'ll|ll|\\s+was)|where(?:'s|s|\\s+is)|who(?:'s|s|\\s+is|\\s+are|'re|re)(?:\\s+\\w+){0,3}|is\\s+there(?:\\s+a)?|any|what(?:'s|s|\\s+is|\\s+are|'re|re))";
const whRecencyFrames = "(?:what(?:'s|s|\\s+is|\\s+are|'re|re|\\s+have|'ve|ve|\\s+has)|where(?:'s|s|\\s+is)|how(?:'s|s|\\s+is))";
const stillVerbs = "(?:available|working|work|supported|in\\s+development|offering|offered|exist|exists|open|active|maintained|relevant|valid|alive|around|running|in\\s+business|used|use|buy|get|find|access|true|a\\s+thing)";
const standaloneStillVerbs = "(?:supported|available|active|maintained|open|in\\s+development|working|running|alive|relevant|in\\s+business|offered|offering|valid|exist|exists|used)";
// Verification building blocks
const verificationNounsStrong = "(?:news|reports?|rumors?|announcements?|stor(?:y|ies))";
const verificationNounsWeak = "(?:claims?|information|info)";
const verificationPredicatesTemporal = "(?:still\\s+true|true\\s+anymore|anymore\\s+true|currently\\s+true|true\\s+now|still\\s+valid|valid\\s+anymore|still\\s+accurate|accurate\\s+anymore|still\\s+correct|correct\\s+anymore)";
const verificationPredicatesGeneral = "(?:true|valid|accurate|correct|real|confirmed|a\\s+hoax|fake)";
// Past year pattern (e.g. 1800-2025) to prevent past-tense verification queries on historical dates from matching
const historicalPastYearAnchor = "\\b(?:1[89]\\d{2}|20[01]\\d|202[0-5])\\b";
// Vision / image description exclusion pattern to prevent image analysis queries (e.g., "what's happening in that image / here") from triggering time dependency
const imageExclusionNouns = "(?:images?|pics?|pictures?|photos?|photographs?|screenshots?|diagrams?|figures?|illustrations?|drawings?|flowcharts?|schematics?|charts?)";
const imageExclusionDeterminers = "(?:this|that|these|those|the|an?|my|our|above|attached|selected|provided)";
const imageExclusionLookahead = `(?!\\s+(?:(?:in|inside|on|with)\\s+(?:${imageExclusionDeterminers}\\s+)*${imageExclusionNouns}(?:\\s+(?:above|below))?\\b|here\\b|in\\s+here\\b))`;
const recencyNewsPrefix = `(?:${inquiryFrames}\\s+(?:${allDeterminers}\\s+)?(?:${recencyAdjectives}\\s+)?(?:${qualityModifiers}\\s+)?|${standaloneDeterminers}\\s+(?:${recencyAdjectives}\\s+)?(?:${qualityModifiers}\\s+)?|${recencyAdjectives}\\s+(?:${qualityModifiers}\\s+)?)`;
export const recencyRuleGroup = new TimeRuleGroup({
category: "Recency",
terms: [
"latest",
"recent",
"recently",
"upcoming",
"breaking",
"live",
"currently",
"up-to-date",
"up to date",
"real-time",
"realtime",
"at present",
"as of now",
"at the moment",
"nowadays",
"these days",
"latest version",
"newest version",
"current version",
"new stable release",
"latest release",
"newest release",
"bring me up to date",
"keep me up to date",
"keep me updated",
"update me on",
"how are things with",
"current status of",
"current state of",
"present state of",
"latest state of",
"latest developments",
"recent developments",
"most recent",
"most recently",
"more recently",
"most up-to-date",
"most up to date",
"most current",
],
patterns: [
{
description: "current <metric/context>",
regex: /\bcurrent\s+(time|date|weather|news|price|version|status|rate|score|year|month|week|day|season)\b/i,
},
{
description: "current/present/latest state <preposition> <topic>",
regex: /\b(?:current|present|latest)\s+state\s+(?:of|in|for|on|about|around|regarding)\b/i,
},
{
description: "is/are/does/can/could/whats/whos ... still <status/verb>",
regex: new RegExp(`\\b(?:is|are|does|do|can|could|what(?:'s|s|\\s+is)|who(?:'s|s|\\s+is))\\s+(?:\\S+\\s+){0,6}still\\s+(?:${mannerAdverbs}\\s+)?${stillVerbs}\\b`, "i"),
},
{
description: "still supported/available/active/open",
regex: new RegExp(`\\bstill\\s+(?:${mannerAdverbs}\\s+)?${standaloneStillVerbs}\\b`, "i"),
},
{
description: "is/are/does it still work/matter/apply",
regex: /\b(?:is|are|does|do)\s+it\s+still\s+(?:work|matter|apply|hold\s+true)\b/i,
},
{
description: "most/more recent/up-to-date/current",
regex: /\b(?:most|more)\s+(?:recent|recently|up-to-date|up\s+to\s+date|current)\b/i,
},
{
description: "upcoming <entity>/intent",
regex: /\b(?:(?:what(?:'s|s|\s+is|\s+are|'re|re)|when(?:'s|s|\s+is|\s+will|'ll|ll)|where(?:'s|s|\s+is)|(?:is|are|was)\s+there(?:\s+an?)?)\s+(?:the\s+|an?\s+)?upcoming|any\s+upcoming|upcoming\s+(?:releases?|updates?|events?|seasons?|episodes?|movies?|games?|elections?|changes?|models?|versions?|features?|matches?|conferences?|launches?|keynotes?))\b/i,
},
{
description: "what's / anything new <with/in/about/on/of/around/regarding/to/for> <topic>",
regex: /\b(?:what(?:'s|s|\s+is)|anything)\s+new\s+(?:with|in|about|on|of|around|regarding|to|for)\s+\S+/i,
},
{
description: "what's (the) <recency> <modifier> news/update/changes/developments/reports <preposition> <topic>",
regex: new RegExp(`\\b${whRecencyFrames}\\s+(?:the\\s+)?(?:${recencyAdjectives}\\s+)?(?:${qualityModifiers}\\s+)?${strongRecencyNouns}\\s+${topicalPrepositions}\\s+\\S+`, "i"),
},
{
description: "what's changed / what has changed <preposition> <topic>",
regex: /\b(?:what|how)(?:'s|s|\s+has|\s+is)?\s+(?:(?:\S+\s+){0,3}changed\b|changed\s+(?:with|in|about|on|of|around|regarding|to|for)\s+\S+)/i,
},
{
description: "any / is there <recency/modifier> news/updates/developments/changes/reports <preposition> <topic>",
regex: new RegExp(`\\b${recencyNewsPrefix}${strongRecencyNouns}\\s+${topicalPrepositions}\\s+\\S+`, "i"),
},
{
description: "latest/current/recent/new <modifier> news/updates/changes/developments/reports/details <preposition> <topic>",
regex: new RegExp(`\\b${recencyAdjectives}\\s+(?:${qualityModifiers}\\s+)?(?:${strongRecencyNouns}|${weakInformationalNouns})\\s+${topicalPrepositions}\\s+\\S+`, "i"),
},
{
description: "contextual new/next <release_modifier> <season/episode/release/version/model/update>",
regex: new RegExp(
`\\b(?:${releaseInquiryFrames}\\s+(?:the\\s+)?(?:new|next)\\s+(?:${releaseModifiers}\\s+)?${releaseEntities}|the\\s+(?:new|next)\\s+(?:${releaseModifiers}\\s+)?${releaseEntities}\\s+${topicalPrepositions}\\s+\\S+|(?:new|next)\\s+(?:season|episode)\\s+${topicalPrepositions}\\s+\\S+)\\b`,
"i"
),
},
{
description: "what's going on / happening / happened / new (in/with ...)",
regex: new RegExp(
`\\bwhat(?:'s|s|\\s+has|\\s+is)\\s+(?:going\\s+on|happening|happened|new)${imageExclusionLookahead}\\b`,
"i"
),
},
{
description: "what happened (today/recently/overnight...)",
regex: /\bwhat\s+happened\s+(today|yesterday|this\s+week|this\s+morning|this\s+afternoon|this\s+evening|tonight|overnight|recently|lately)\b/i,
},
{
description: "what's the latest / status of ...",
regex: /\bwhat(?:'s|s|\s+is)\s+the\s+(?:(?:\S+\s+){0,2})?(latest|status)\b/i,
},
{
description: "present news/report/rumor verification (is/are this/that/the news/report ... true/valid/real)",
regex: new RegExp(
`\\b(?:is|are)\\s+(?:the|this|that|these|those)\\s+${verificationNounsStrong}\\s+(?:\\S+\\s+){0,10}(?:${verificationPredicatesTemporal}|${verificationPredicatesGeneral})\\b`,
"i"
),
},
{
description: "past news/report/rumor verification (was/were this/that/the news/report ... true/valid/real)",
regex: new RegExp(
`^(?!.*${historicalPastYearAnchor})\\b(?:was|were)\\s+(?:the|this|that|these|those)\\s+${verificationNounsStrong}\\s+(?:\\S+\\s+){0,10}(?:${verificationPredicatesTemporal}|${verificationPredicatesGeneral})\\b`,
"i"
),
},
{
description: "present claim/information verification (is/are this/that/the claim/info ... still true/valid)",
regex: new RegExp(
`\\b(?:is|are)\\s+(?:the|this|that|these|those)\\s+${verificationNounsWeak}\\s+(?:\\S+\\s+){0,10}${verificationPredicatesTemporal}\\b`,
"i"
),
},
{
description: "past claim/information verification (was/were this/that/the claim/info ... still true/valid)",
regex: new RegExp(
`^(?!.*${historicalPastYearAnchor})\\b(?:was|were)\\s+(?:the|this|that|these|those)\\s+${verificationNounsWeak}\\s+(?:\\S+\\s+){0,10}${verificationPredicatesTemporal}\\b`,
"i"
),
},
],
});
// ─────────────────────────────────────────────────────────────────────────────
// 3. Service Outages Group (High-priority position)
// ─────────────────────────────────────────────────────────────────────────────
export const serviceOutagesRuleGroup = new TimeRuleGroup({
category: "Service Outages",
terms: [
"outage",
"service status",
],
patterns: [
{
description: "(is/was) <service> <adverb> down/working/offline/up",
regex: /\b(?:(?:is|was)\s+)?\S+\s+(?:currently\s+|still\s+|temporarily\s+)?(down|working|offline|up)\b/i,
},
],
});
// ─────────────────────────────────────────────────────────────────────────────
// 4. Weather Group
// ─────────────────────────────────────────────────────────────────────────────
export const weatherRuleGroup = new TimeRuleGroup({
category: "Weather",
terms: [
"weather today",
"today's weather",
"todays weather",
"current weather",
"weather now",
"tomorrow's weather",
"tomorrows weather",
"temperature today",
"rain today",
],
patterns: [
{
description: "<weather/temperature/forecast/rain/snow> <today/tomorrow/now/this week>",
regex: /\b(weather|temperature|forecast|rain|snow)\s+(today|tomorrow|now|this\s+week)\b/i,
},
],
});
// ─────────────────────────────────────────────────────────────────────────────
// 5. Business Hours & Availability Group
// ─────────────────────────────────────────────────────────────────────────────
export const businessHoursAvailabilityRuleGroup = new TimeRuleGroup({
category: "Business Hours & Availability",
terms: [
"open now",
"closed now",
"opening hours",
"business hours",
"open today",
"closed today",
"closing time",
"holiday hours",
"available now",
"available today",
"in stock",
"sold out",
],
patterns: [
{
description: "<open/closed/opening/closing> <hours/today/now/time>",
regex: /\b(open|closed|opening|closing)\s+(hours|today|now|time)\b/i,
},
],
});
// ─────────────────────────────────────────────────────────────────────────────
// 6. Live Scores & Sports Group
// ─────────────────────────────────────────────────────────────────────────────
export const liveScoresSportsRuleGroup = new TimeRuleGroup({
category: "Live Scores & Sports",
terms: [
"score today",
"live score",
"live match",
"match today",
"playing now",
"won today",
"current standings",
"league standings",
"standings today",
"points table",
"current fixtures",
"fixtures today",
"schedule today",
"next match",
"game today",
"match score",
],
patterns: [
{
description: "<score/playing/won/match> <today/now/live>",
regex: /\b(score|live\s+score|playing|won|match)\s+(today|now|live)\b/i,
},
],
});
// ─────────────────────────────────────────────────────────────────────────────
// 7. Financial Markets Group
// ─────────────────────────────────────────────────────────────────────────────
export const financialMarketsRuleGroup = new TimeRuleGroup({
category: "Financial Markets",
terms: [
"stock price",
"share price",
"crypto price",
"market cap",
"current exchange rate",
"exchange rate today",
"bitcoin price",
"btc price",
"eth price",
"gold price",
"silver price",
"market today",
"share market today",
"market open",
"market closed",
"trading today",
"year to date",
"year-to-date",
"month to date",
"month-to-date",
"quarter to date",
"quarter-to-date",
"current fiscal year",
"this fiscal year",
"current quarter",
"this fiscal quarter",
"fiscal year to date",
],
patterns: [
{
description: "<stock/share/crypto/bitcoin/btc/eth/gold/silver> price",
regex: /\b(stock|share|crypto|bitcoin|btc|eth|gold|silver)\s+price\b/i,
},
{
description: "current exchange rate / exchange rate today",
regex: /\b(current\s+exchange\s+rate|exchange\s+rate\s+today)\b/i,
},
{
description: "ytd/mtd/qtd performance/metrics",
regex: /\b(?:ytd|mtd|qtd)\s+(?:performance|return|returns|growth|revenue|sales|profit|loss|gain|figures?|results?|earnings|numbers?|data)\b/i,
},
{
description: "revenue/sales/earnings ytd/mtd/qtd",
regex: /\b(?:revenue|sales|earnings|returns?|performance|growth)\s+(?:ytd|mtd|qtd)\b/i,
},
{
description: "year/month/quarter to date",
regex: /\b(?:year|month|quarter)\s+to\s+date\b/i,
},
{
description: "current/this fiscal year/quarter",
regex: /\b(?:current|this)\s+fiscal\s+(?:year|quarter)\b/i,
},
],
});
// ─────────────────────────────────────────────────────────────────────────────
// 8. Transportation & Logistics Group
// ─────────────────────────────────────────────────────────────────────────────
export const transportationRuleGroup = new TimeRuleGroup({
category: "Transportation & Logistics",
terms: [
"flight status",
"train status",
"bus status",
"flight tracker",
"eta",
"expected arrival",
"departure delay",
"delayed",
"arriving",
"departed",
"traffic now",
"traffic today",
"current traffic",
"traffic conditions",
"road closed",
"road conditions",
],
patterns: [
{
description: "<flight/train/bus> <status/delayed/arriving/departed>",
regex: /\b(flight|train|bus)\s+(status|delayed|arriving|departed|today|now)\b/i,
},
],
});
// ─────────────────────────────────────────────────────────────────────────────
// 9. Event Status Group
// ─────────────────────────────────────────────────────────────────────────────
export const eventStatusRuleGroup = new TimeRuleGroup({
category: "Event Status",
patterns: [
{
description: "has/did ... started|ended|finished|begun|open|opened|close|closed",
regex: /\b(has|did)\s+.+\s+(started|ended|finished|begun|open|opened|close|closed)\b/i,
},
{
description: "is/was ... happening|ongoing|live|underway",
regex: /\b(?:is|was)\s+.+\s+(happening|ongoing|live|underway)\b/i,
},
{
description: "when does/is/will/whens ... start|end|release|launch|arrive|coming out|available|expected",
regex: /\bwhen(?:'s|s|\s+does|\s+is|\s+will|'ll|ll|\s+did)\s+(?:\S+\s+){0,6}(?:start|starting|started|end|ending|ended|finish|finishing|finished|begin|beginning|coming\s+out|expected\s+to\s+(?:release|launch|arrive|start|begin)|expected|(?:be\s+)?(?:released|available|launched|ready)|release|releasing|launch|launching|arrive|arriving|drop|dropping|air|airing)\b/i,
},
{
description: "when is/was/will the <event/match/final/conference/election/deadline/launch>",
regex: /\bwhen(?:'s|s|\s+is|\s+was|\s+will|'ll|ll)\s+(?:the\s+)?(?:next\s+)?(?:\S+\s+){0,2}(final|game|match|event|conference|keynote|election|deadline|launch|webinar|meeting|hearing|summit|eclipses?|season|episode|release|version)\b/i,
},
],
});
// ─────────────────────────────────────────────────────────────────────────────
// 10. Elections & Results Group
// ─────────────────────────────────────────────────────────────────────────────
export const electionsResultsRuleGroup = new TimeRuleGroup({
category: "Elections & Results",
terms: [
"election result",
"election results",
"who won",
"who's winning",
"whos winning",
"leading now",
"who's leading",
"whos leading",
"leading in the election",
"projected winner",
"exit poll",
],
patterns: [
{
description: "<election/poll/vote> <results/winner/leading/status>",
regex: /\b(election|poll|vote)\s+(results?|winner|leading|status)\b/i,
},
{
description: "who is / who's / whos / who was / who will winning or leading or win",
regex: /\bwho(?:'s|s|\s+is|\s+was|\s+will|'ll|ll)\s+(?:currently\s+)?(win|winning|lead|leading|won)\b/i,
},
],
});
// ─────────────────────────────────────────────────────────────────────────────
// 11. Office Holders Group (Pattern-matched)
// Data list of role titles dynamically wrapped in query intent patterns
// ─────────────────────────────────────────────────────────────────────────────
export const officeHolderRoles: readonly string[] = [
"chief of defence staff",
"cds",
"chief of army staff",
"coas",
"chief of naval staff",
"cns",
"chief of air staff",
"cas",
"national security advisor",
"nsa",
"attorney general",
"solicitor general",
"prime minister",
"pm of",
"chief minister",
"cm of",
"cm",
"home minister",
"foreign minister",
"finance minister",
"defense minister",
"law minister",
"cabinet minister",
"minister of",
"president of",
"president",
"vice president",
"chief justice",
"governor of",
"governor",
"ceo",
"cto",
"cfo",
"mayor",
"captain",
"coach",
"chancellor",
"monarch",
"king",
"queen",
"head of state",
"chairman",
"secretary general",
"speaker of the house",
];
const sortedOfficeHolderRoles = [...officeHolderRoles].sort((a, b) => b.length - a.length);
const rolesSubPattern = sortedOfficeHolderRoles.map(r => escapeRegExp(r)).join("|");
export const officeHoldersRuleGroup = new TimeRuleGroup({
category: "Office Holders",
patterns: [
{
description: "Who is the <office holder> of ...",
regex: new RegExp(`\\bwho(?:'s|s|\\s+is)\\s+(?:the\\s+)?(?:current\\s+)?(${rolesSubPattern})\\b`, "i"),
},
{
description: "Current <office holder> of ...",
regex: new RegExp(`\\bcurrent\\s+(${rolesSubPattern})\\b`, "i"),
},
],
});
/**
* Ordered list of rule groups evaluated sequentially by expected real-world query frequency.
*/
export const timeDependencyRuleGroups: readonly TimeRuleGroup[] = [
relativeTimeRuleGroup,
recencyRuleGroup,
serviceOutagesRuleGroup,
weatherRuleGroup,
businessHoursAvailabilityRuleGroup,
liveScoresSportsRuleGroup,
financialMarketsRuleGroup,
transportationRuleGroup,
eventStatusRuleGroup,
electionsResultsRuleGroup,
officeHoldersRuleGroup,
];
export const DETECTOR_VERSION = "1.5";
export interface TimeDependencyOptions {
requestId?: string;
totalRequestTimeMs?: number;
}
/**
* Evaluates whether a query is a conceptual, educational, or descriptive question structure
* using indefinite noun phrases (e.g., "how does a recent update work?", "what's a new version in semantic versioning?").
*/
export function isConceptualOrDescriptiveQuery(text: string): boolean {
if (!text || typeof text !== "string") {
return false;
}
const conceptualPatterns = [
/\bhow\s+does\s+(?:a|an)\s+.+\s+works?\b/i,
/\bhow\s+do\s+(?:a|an)\s+.+\s+work\b/i,
/\bhow(?:'s|s|\s+is)\s+(?:a|an)\s+.+\s+works?\b/i,
/\bhow\s+to\s+(?:build|create|write|implement|design|develop|make|construct|install|configure|set\s+up|use)\s+(?:a|an)\b/i,
/\bwhat(?:'s|s|\s+is|\s+defines|\s+constitutes)\s+(?:a|an)\b/i,
/\bexplain\s+(?:how|the\s+concept\s+of|what(?:'s|s|\s+is)?\s+(?:a|an)?)\b/i,
/\b(?:definition|meaning)\s+of\s+(?:a|an)\b/i,
];
return conceptualPatterns.some(pattern => pattern.test(text));
}
/**
* Evaluates user prompt text against all time dependency rule groups.
* Returns true if any rule group matches.
*/
export function requiresCurrentTimeContext(
userText: string,
options?: string | TimeDependencyOptions
): boolean {
if (!userText || typeof userText !== "string") {
return false;
}
if (isConceptualOrDescriptiveQuery(userText)) {
return false;
}
return timeDependencyRuleGroups.some(rg => rg.matches(userText));
}
function escapeRegExp(str: string): string {
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function capitalizeWords(str: string): string {
return str
.split(" ")
.map(w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
.join(" ");
}
export interface TimePatternRule {
readonly description: string;
readonly regex: RegExp;
}
export interface TimeRuleGroupConfig {
readonly category: string;
/** Standalone phrases/words matched with word boundaries (e.g., "today", "this afternoon", "stock price"). */
readonly terms?: readonly string[];
/** Pattern-based rules for structural queries (e.g., relative offsets like "3 days ago" or role patterns). */
readonly patterns?: readonly TimePatternRule[];
}
export class TimeRuleGroup {
readonly category: string;
readonly terms: readonly string[];
readonly patterns: readonly TimePatternRule[];
private readonly compiledTermRegexes: readonly { term: string; regex: RegExp }[];
constructor(config: TimeRuleGroupConfig) {
this.category = config.category;
this.terms = config.terms ?? [];
this.patterns = config.patterns ?? [];
this.compiledTermRegexes = this.terms.map(term => ({
term,
regex: new RegExp(`\\b${escapeRegExp(term.trim())}\\b`, "i"),
}));
}
public matches(text: string): boolean {
return (
this.compiledTermRegexes.some(tr => tr.regex.test(text)) ||
this.patterns.some(p => p.regex.test(text))
);
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Dynamic Year Detection Helper
// ─────────────────────────────────────────────────────────────────────────────
/**
* Dynamically builds a regex alternation pattern matching the current 4-digit year
* and any future 4-digit years (e.g., 2026 -> 202[6-9]|20[3-9]\d|2[1-9]\d{2}|[3-9]\d{3}).
*/
export function buildCurrentAndFutureYearPattern(currentYear: number = new Date().getFullYear()): string {
const yStr = String(currentYear);
if (yStr.length !== 4) {
return `${currentYear}|202[6-9]|20[3-9]\\d|2[1-9]\\d{2}|[3-9]\\d{3}`;
}
const d0 = parseInt(yStr[0], 10);
const d1 = parseInt(yStr[1], 10);
const d2 = parseInt(yStr[2], 10);
const d3 = parseInt(yStr[3], 10);
const branches: string[] = [];
// 1. Same d0, d1, d2, and units from d3 to 9
if (d3 === 9) {
branches.push(`${d0}${d1}${d2}9`);
} else {
branches.push(`${d0}${d1}${d2}[${d3}-9]`);
}
// 2. Same d0, d1, and tens from (d2 + 1) to 9
if (d2 < 9) {
const nextTens = d2 + 1;
if (nextTens === 9) {
branches.push(`${d0}${d1}9\\d`);
} else {
branches.push(`${d0}${d1}[${nextTens}-9]\\d`);
}
}
// 3. Same d0, and hundreds from (d1 + 1) to 9
if (d1 < 9) {
const nextHundreds = d1 + 1;
if (nextHundreds === 9) {
branches.push(`${d0}9\\d{2}`);
} else {
branches.push(`${d0}[${nextHundreds}-9]\\d{2}`);
}
}
// 4. Thousands from (d0 + 1) to 9
if (d0 < 9) {
const nextThousands = d0 + 1;
if (nextThousands === 9) {
branches.push(`9\\d{3}`);
} else {
branches.push(`[${nextThousands}-9]\\d{3}`);
}
}
return branches.join("|");
}
export const currentAndFutureYearPattern = buildCurrentAndFutureYearPattern();
// ─────────────────────────────────────────────────────────────────────────────
// 1. Relative Time Group
// ─────────────────────────────────────────────────────────────────────────────
const relativeDayParts = "(?:morning|afternoon|evening|night|noon|midday|forenoon)";
const relativePeriods = "(?:week|weekend|month|quarter|year|semester|term|decade|century)";
const relativeSeasons = "(?:season|summer|winter|spring|autumn|fall|monsoon)";
export const relativeTimeRuleGroup = new TimeRuleGroup({
category: "Relative Time",
terms: [
"today",
"tomorrow",
"yesterday",
"tonight",
"right now",
"overnight",
"local time",
"time zone",
"timezone",
"what day is it",
],
patterns: [
{
description: "this morning / afternoon / evening / night / noon / midday / forenoon",
regex: new RegExp(`\\bthis\\s+${relativeDayParts}\\b`, "i"),
},
{
description: "this|next|last week / weekend / month / quarter / year / semester / term / decade / century",
regex: new RegExp(`\\b(?:this|next|last)\\s+${relativePeriods}\\b`, "i"),
},
{
description: "this|next|last season / summer / winter / spring / autumn / fall / monsoon",
regex: new RegExp(`\\b(?:this|next|last)\\s+${relativeSeasons}\\b`, "i"),
},
{
description: "so far today / tonight / this <day part / period / season>",
regex: new RegExp(
`\\bso\\s+far\\s+(?:today|tonight|this\\s+(?:${relativeDayParts}|${relativePeriods}|${relativeSeasons}))\\b`,
"i"
),
},
{
description: "as of today / tonight / yesterday / last night / this|last <day part / period / season>",
regex: new RegExp(
`\\bas\\s+of\\s+(?:today|tonight|yesterday|last\\s+night|this\\s+${relativeDayParts}|(?:this|last)\\s+(?:${relativePeriods}|${relativeSeasons}))\\b`,
"i"
),
},
{
description: "this|next|last <weekday>",
regex: /\b(this|next|last)\s+(monday|tuesday|wednesday|thursday|friday|saturday|sunday)\b/i,
},
{
description: "N days ago / a few days ago",
regex: /\b(?:\d+|a\s+few|a\s+couple(?:\s+of)?|several|a|one)\s+(?:sec|second|min|minute|hour|day|week|month|year)s?\s+ago\b/i,
},
{
description: "what time is it",
regex: /\bwhat(?:'s|s|\s+is)?\s+(?:the\s+)?(?:time|date|day|year|month)\b/i,
},
{
description: "temporal year anchor (in/for/during/throughout/as of/so far in/since/by/from <current/future year>)",
regex: new RegExp(
`\\b(?:so\\s+far\\s+in|as\\s+of|throughout|during|in|for|since|by|from)\\s+(?:${currentAndFutureYearPattern})\\b`,
"i"
),
},
{
description: "as of/in/for/since/during/by/from/throughout <month> <current/future year>",
regex: new RegExp(
`\\b(?:as\\s+of|in|for|since|during|by|from|throughout)\\s+(?:january|february|march|april|may|june|july|august|september|october|november|december|jan|feb|mar|apr|jun|jul|aug|sep|sept|oct|nov|dec)\\s+(?:\\d{1,2},?\\s+)?(?:${currentAndFutureYearPattern})\\b`,
"i"
),
},
{
description: "rolling past duration window (in/over/during/within/for/throughout the last/past N units)",
regex: /\b(?:(?:in|over|during|within|for|throughout)\s+(?:the\s+)?)?(?:last|past|previous)\s+(?:\d+|a|an|few|couple(?:\s+of)?|several)\s+(?:sec|second|min|minute|hour|day|week|month|year)s?\b|\b(?:in|over|during|within|for|throughout)\s+recent\s+(?:days|weeks|months|years|hours)\b/i,
},
{
description: "relative future duration (in/within N units or over/during/for/throughout next N units)",
regex: /\b(?:in|within)\s+(?:(?:the\s+)?(?:next|coming)\s+)?(?:\d+|two|three|four|five|six|seven|eight|nine|ten|a|an|few|couple(?:\s+of)?|several)\s+(?:sec|second|min|minute|hour|day|week|month|year)s?\b|\b(?:over|during|for|throughout)\s+(?:the\s+)?(?:next|coming)\s+(?:\d+|two|three|four|five|six|seven|eight|nine|ten|a|an|few|couple(?:\s+of)?|several)\s+(?:sec|second|min|minute|hour|day|week|month|year)s?\b/i,
},
{
description: "how long / much longer until/before",
regex: /\bhow\s+(?:long|much\s+longer)\s+(?:until|till|before)\b/i,
},
{
description: "how much time left / until",
regex: /\bhow\s+much\s+time\s+(?:is\s+left|until|till|remaining|left)\b/i,
},
],
});
// ─────────────────────────────────────────────────────────────────────────────
// 2. Recency Group (Includes Version & Releases)
// ─────────────────────────────────────────────────────────────────────────────
const inquiryFrames = "(?:(?:is|are|was)\\s+there|do\\s+you\\s+have|do\\s+we\\s+have|got|have\\s+you\\s+heard)";
const allDeterminers = "(?:any|some|more|much|the|a|an)";
const standaloneDeterminers = "(?:any|some|more|much)";
const recencyAdjectives = "(?:new|latest|recent|most\\s+recent|fresh|breaking|upcoming)";
const qualityModifiers = "(?:good|bad|positive|negative|exciting|promising|major|important|significant|big|huge|massive|critical|key|main)";
const releaseModifiers = "(?:major|big|significant|main|stable|minor)";
const mannerAdverbs = "(?:officially|widely|actively|fully|currently|generally|publicly|commercially)";
const strongRecencyNouns = "(?:news|updates?|developments?|changes?|reports?)";
const weakInformationalNouns = "(?:details?|coverage|info|information)";
const topicalPrepositions = "(?:on|about|of|with|for|regarding|in|around|to)";
const releaseEntities = "(?:season|episode|release|version|model|edition|generation|update)";
const releaseInquiryFrames = "(?:when(?:'s|s|\\s+is|\\s+will|'ll|ll|\\s+was)|where(?:'s|s|\\s+is)|who(?:'s|s|\\s+is|\\s+are|'re|re)(?:\\s+\\w+){0,3}|is\\s+there(?:\\s+a)?|any|what(?:'s|s|\\s+is|\\s+are|'re|re))";
const whRecencyFrames = "(?:what(?:'s|s|\\s+is|\\s+are|'re|re|\\s+have|'ve|ve|\\s+has)|where(?:'s|s|\\s+is)|how(?:'s|s|\\s+is))";
const stillVerbs = "(?:available|working|work|supported|in\\s+development|offering|offered|exist|exists|open|active|maintained|relevant|valid|alive|around|running|in\\s+business|used|use|buy|get|find|access|true|a\\s+thing)";
const standaloneStillVerbs = "(?:supported|available|active|maintained|open|in\\s+development|working|running|alive|relevant|in\\s+business|offered|offering|valid|exist|exists|used)";
// Verification building blocks
const verificationNounsStrong = "(?:news|reports?|rumors?|announcements?|stor(?:y|ies))";
const verificationNounsWeak = "(?:claims?|information|info)";
const verificationPredicatesTemporal = "(?:still\\s+true|true\\s+anymore|anymore\\s+true|currently\\s+true|true\\s+now|still\\s+valid|valid\\s+anymore|still\\s+accurate|accurate\\s+anymore|still\\s+correct|correct\\s+anymore)";
const verificationPredicatesGeneral = "(?:true|valid|accurate|correct|real|confirmed|a\\s+hoax|fake)";
// Past year pattern (e.g. 1800-2025) to prevent past-tense verification queries on historical dates from matching
const historicalPastYearAnchor = "\\b(?:1[89]\\d{2}|20[01]\\d|202[0-5])\\b";
// Vision / image description exclusion pattern to prevent image analysis queries (e.g., "what's happening in that image / here") from triggering time dependency
const imageExclusionNouns = "(?:images?|pics?|pictures?|photos?|photographs?|screenshots?|diagrams?|figures?|illustrations?|drawings?|flowcharts?|schematics?|charts?)";
const imageExclusionDeterminers = "(?:this|that|these|those|the|an?|my|our|above|attached|selected|provided)";
const imageExclusionLookahead = `(?!\\s+(?:(?:in|inside|on|with)\\s+(?:${imageExclusionDeterminers}\\s+)*${imageExclusionNouns}(?:\\s+(?:above|below))?\\b|here\\b|in\\s+here\\b))`;
const recencyNewsPrefix = `(?:${inquiryFrames}\\s+(?:${allDeterminers}\\s+)?(?:${recencyAdjectives}\\s+)?(?:${qualityModifiers}\\s+)?|${standaloneDeterminers}\\s+(?:${recencyAdjectives}\\s+)?(?:${qualityModifiers}\\s+)?|${recencyAdjectives}\\s+(?:${qualityModifiers}\\s+)?)`;
export const recencyRuleGroup = new TimeRuleGroup({
category: "Recency",
terms: [
"latest",
"recent",
"recently",
"upcoming",
"breaking",
"live",
"currently",
"up-to-date",
"up to date",
"real-time",
"realtime",
"at present",
"as of now",
"at the moment",
"nowadays",
"these days",
"latest version",
"newest version",
"current version",
"new stable release",
"latest release",
"newest release",
"bring me up to date",
"keep me up to date",
"keep me updated",
"update me on",
"how are things with",
"current status of",
"current state of",
"present state of",
"latest state of",
"latest developments",
"recent developments",
"most recent",
"most recently",
"more recently",
"most up-to-date",
"most up to date",
"most current",
],
patterns: [
{
description: "current <metric/context>",
regex: /\bcurrent\s+(time|date|weather|news|price|version|status|rate|score|year|month|week|day|season)\b/i,
},
{
description: "current/present/latest state <preposition> <topic>",
regex: /\b(?:current|present|latest)\s+state\s+(?:of|in|for|on|about|around|regarding)\b/i,
},
{
description: "is/are/does/can/could/whats/whos ... still <status/verb>",
regex: new RegExp(`\\b(?:is|are|does|do|can|could|what(?:'s|s|\\s+is)|who(?:'s|s|\\s+is))\\s+(?:\\S+\\s+){0,6}still\\s+(?:${mannerAdverbs}\\s+)?${stillVerbs}\\b`, "i"),
},
{
description: "still supported/available/active/open",
regex: new RegExp(`\\bstill\\s+(?:${mannerAdverbs}\\s+)?${standaloneStillVerbs}\\b`, "i"),
},
{
description: "is/are/does it still work/matter/apply",
regex: /\b(?:is|are|does|do)\s+it\s+still\s+(?:work|matter|apply|hold\s+true)\b/i,
},
{
description: "most/more recent/up-to-date/current",
regex: /\b(?:most|more)\s+(?:recent|recently|up-to-date|up\s+to\s+date|current)\b/i,
},
{
description: "upcoming <entity>/intent",
regex: /\b(?:(?:what(?:'s|s|\s+is|\s+are|'re|re)|when(?:'s|s|\s+is|\s+will|'ll|ll)|where(?:'s|s|\s+is)|(?:is|are|was)\s+there(?:\s+an?)?)\s+(?:the\s+|an?\s+)?upcoming|any\s+upcoming|upcoming\s+(?:releases?|updates?|events?|seasons?|episodes?|movies?|games?|elections?|changes?|models?|versions?|features?|matches?|conferences?|launches?|keynotes?))\b/i,
},
{
description: "what's / anything new <with/in/about/on/of/around/regarding/to/for> <topic>",
regex: /\b(?:what(?:'s|s|\s+is)|anything)\s+new\s+(?:with|in|about|on|of|around|regarding|to|for)\s+\S+/i,
},
{
description: "what's (the) <recency> <modifier> news/update/changes/developments/reports <preposition> <topic>",
regex: new RegExp(`\\b${whRecencyFrames}\\s+(?:the\\s+)?(?:${recencyAdjectives}\\s+)?(?:${qualityModifiers}\\s+)?${strongRecencyNouns}\\s+${topicalPrepositions}\\s+\\S+`, "i"),
},
{
description: "what's changed / what has changed <preposition> <topic>",
regex: /\b(?:what|how)(?:'s|s|\s+has|\s+is)?\s+(?:(?:\S+\s+){0,3}changed\b|changed\s+(?:with|in|about|on|of|around|regarding|to|for)\s+\S+)/i,
},
{
description: "any / is there <recency/modifier> news/updates/developments/changes/reports <preposition> <topic>",
regex: new RegExp(`\\b${recencyNewsPrefix}${strongRecencyNouns}\\s+${topicalPrepositions}\\s+\\S+`, "i"),
},
{
description: "latest/current/recent/new <modifier> news/updates/changes/developments/reports/details <preposition> <topic>",
regex: new RegExp(`\\b${recencyAdjectives}\\s+(?:${qualityModifiers}\\s+)?(?:${strongRecencyNouns}|${weakInformationalNouns})\\s+${topicalPrepositions}\\s+\\S+`, "i"),
},
{
description: "contextual new/next <release_modifier> <season/episode/release/version/model/update>",
regex: new RegExp(
`\\b(?:${releaseInquiryFrames}\\s+(?:the\\s+)?(?:new|next)\\s+(?:${releaseModifiers}\\s+)?${releaseEntities}|the\\s+(?:new|next)\\s+(?:${releaseModifiers}\\s+)?${releaseEntities}\\s+${topicalPrepositions}\\s+\\S+|(?:new|next)\\s+(?:season|episode)\\s+${topicalPrepositions}\\s+\\S+)\\b`,
"i"
),
},
{
description: "what's going on / happening / happened / new (in/with ...)",
regex: new RegExp(
`\\bwhat(?:'s|s|\\s+has|\\s+is)\\s+(?:going\\s+on|happening|happened|new)${imageExclusionLookahead}\\b`,
"i"
),
},
{
description: "what happened (today/recently/overnight...)",
regex: /\bwhat\s+happened\s+(today|yesterday|this\s+week|this\s+morning|this\s+afternoon|this\s+evening|tonight|overnight|recently|lately)\b/i,
},
{
description: "what's the latest / status of ...",
regex: /\bwhat(?:'s|s|\s+is)\s+the\s+(?:(?:\S+\s+){0,2})?(latest|status)\b/i,
},
{
description: "present news/report/rumor verification (is/are this/that/the news/report ... true/valid/real)",
regex: new RegExp(
`\\b(?:is|are)\\s+(?:the|this|that|these|those)\\s+${verificationNounsStrong}\\s+(?:\\S+\\s+){0,10}(?:${verificationPredicatesTemporal}|${verificationPredicatesGeneral})\\b`,
"i"
),
},
{
description: "past news/report/rumor verification (was/were this/that/the news/report ... true/valid/real)",
regex: new RegExp(
`^(?!.*${historicalPastYearAnchor})\\b(?:was|were)\\s+(?:the|this|that|these|those)\\s+${verificationNounsStrong}\\s+(?:\\S+\\s+){0,10}(?:${verificationPredicatesTemporal}|${verificationPredicatesGeneral})\\b`,
"i"
),
},
{
description: "present claim/information verification (is/are this/that/the claim/info ... still true/valid)",
regex: new RegExp(
`\\b(?:is|are)\\s+(?:the|this|that|these|those)\\s+${verificationNounsWeak}\\s+(?:\\S+\\s+){0,10}${verificationPredicatesTemporal}\\b`,
"i"
),
},
{
description: "past claim/information verification (was/were this/that/the claim/info ... still true/valid)",
regex: new RegExp(
`^(?!.*${historicalPastYearAnchor})\\b(?:was|were)\\s+(?:the|this|that|these|those)\\s+${verificationNounsWeak}\\s+(?:\\S+\\s+){0,10}${verificationPredicatesTemporal}\\b`,
"i"
),
},
],
});
// ─────────────────────────────────────────────────────────────────────────────
// 3. Service Outages Group (High-priority position)
// ─────────────────────────────────────────────────────────────────────────────
export const serviceOutagesRuleGroup = new TimeRuleGroup({
category: "Service Outages",
terms: [
"outage",
"service status",
],
patterns: [
{
description: "(is/was) <service> <adverb> down/working/offline/up",
regex: /\b(?:(?:is|was)\s+)?\S+\s+(?:currently\s+|still\s+|temporarily\s+)?(down|working|offline|up)\b/i,
},
],
});
// ─────────────────────────────────────────────────────────────────────────────
// 4. Weather Group
// ─────────────────────────────────────────────────────────────────────────────
export const weatherRuleGroup = new TimeRuleGroup({
category: "Weather",
terms: [
"weather today",
"today's weather",
"todays weather",
"current weather",
"weather now",
"tomorrow's weather",
"tomorrows weather",
"temperature today",
"rain today",
],
patterns: [
{
description: "<weather/temperature/forecast/rain/snow> <today/tomorrow/now/this week>",
regex: /\b(weather|temperature|forecast|rain|snow)\s+(today|tomorrow|now|this\s+week)\b/i,
},
],
});
// ─────────────────────────────────────────────────────────────────────────────
// 5. Business Hours & Availability Group
// ─────────────────────────────────────────────────────────────────────────────
export const businessHoursAvailabilityRuleGroup = new TimeRuleGroup({
category: "Business Hours & Availability",
terms: [
"open now",
"closed now",
"opening hours",
"business hours",
"open today",
"closed today",
"closing time",
"holiday hours",
"available now",
"available today",
"in stock",
"sold out",
],
patterns: [
{
description: "<open/closed/opening/closing> <hours/today/now/time>",
regex: /\b(open|closed|opening|closing)\s+(hours|today|now|time)\b/i,
},
],
});
// ─────────────────────────────────────────────────────────────────────────────
// 6. Live Scores & Sports Group
// ─────────────────────────────────────────────────────────────────────────────
export const liveScoresSportsRuleGroup = new TimeRuleGroup({
category: "Live Scores & Sports",
terms: [
"score today",
"live score",
"live match",
"match today",
"playing now",
"won today",
"current standings",
"league standings",
"standings today",
"points table",
"current fixtures",
"fixtures today",
"schedule today",
"next match",
"game today",
"match score",
],
patterns: [
{
description: "<score/playing/won/match> <today/now/live>",
regex: /\b(score|live\s+score|playing|won|match)\s+(today|now|live)\b/i,
},
],
});
// ─────────────────────────────────────────────────────────────────────────────
// 7. Financial Markets Group
// ─────────────────────────────────────────────────────────────────────────────
export const financialMarketsRuleGroup = new TimeRuleGroup({
category: "Financial Markets",
terms: [
"stock price",
"share price",
"crypto price",
"market cap",
"current exchange rate",
"exchange rate today",
"bitcoin price",
"btc price",
"eth price",
"gold price",
"silver price",
"market today",
"share market today",
"market open",
"market closed",
"trading today",
"year to date",
"year-to-date",
"month to date",
"month-to-date",
"quarter to date",
"quarter-to-date",
"current fiscal year",
"this fiscal year",
"current quarter",
"this fiscal quarter",
"fiscal year to date",
],
patterns: [
{
description: "<stock/share/crypto/bitcoin/btc/eth/gold/silver> price",
regex: /\b(stock|share|crypto|bitcoin|btc|eth|gold|silver)\s+price\b/i,
},
{
description: "current exchange rate / exchange rate today",
regex: /\b(current\s+exchange\s+rate|exchange\s+rate\s+today)\b/i,
},
{
description: "ytd/mtd/qtd performance/metrics",
regex: /\b(?:ytd|mtd|qtd)\s+(?:performance|return|returns|growth|revenue|sales|profit|loss|gain|figures?|results?|earnings|numbers?|data)\b/i,
},
{
description: "revenue/sales/earnings ytd/mtd/qtd",
regex: /\b(?:revenue|sales|earnings|returns?|performance|growth)\s+(?:ytd|mtd|qtd)\b/i,
},
{
description: "year/month/quarter to date",
regex: /\b(?:year|month|quarter)\s+to\s+date\b/i,
},
{
description: "current/this fiscal year/quarter",
regex: /\b(?:current|this)\s+fiscal\s+(?:year|quarter)\b/i,
},
],
});
// ─────────────────────────────────────────────────────────────────────────────
// 8. Transportation & Logistics Group
// ─────────────────────────────────────────────────────────────────────────────
export const transportationRuleGroup = new TimeRuleGroup({
category: "Transportation & Logistics",
terms: [
"flight status",
"train status",
"bus status",
"flight tracker",
"eta",
"expected arrival",
"departure delay",
"delayed",
"arriving",
"departed",
"traffic now",
"traffic today",
"current traffic",
"traffic conditions",
"road closed",
"road conditions",
],
patterns: [
{
description: "<flight/train/bus> <status/delayed/arriving/departed>",
regex: /\b(flight|train|bus)\s+(status|delayed|arriving|departed|today|now)\b/i,
},
],
});
// ─────────────────────────────────────────────────────────────────────────────
// 9. Event Status Group
// ─────────────────────────────────────────────────────────────────────────────
export const eventStatusRuleGroup = new TimeRuleGroup({
category: "Event Status",
patterns: [
{
description: "has/did ... started|ended|finished|begun|open|opened|close|closed",
regex: /\b(has|did)\s+.+\s+(started|ended|finished|begun|open|opened|close|closed)\b/i,
},
{
description: "is/was ... happening|ongoing|live|underway",
regex: /\b(?:is|was)\s+.+\s+(happening|ongoing|live|underway)\b/i,
},
{
description: "when does/is/will/whens ... start|end|release|launch|arrive|coming out|available|expected",
regex: /\bwhen(?:'s|s|\s+does|\s+is|\s+will|'ll|ll|\s+did)\s+(?:\S+\s+){0,6}(?:start|starting|started|end|ending|ended|finish|finishing|finished|begin|beginning|coming\s+out|expected\s+to\s+(?:release|launch|arrive|start|begin)|expected|(?:be\s+)?(?:released|available|launched|ready)|release|releasing|launch|launching|arrive|arriving|drop|dropping|air|airing)\b/i,
},
{
description: "when is/was/will the <event/match/final/conference/election/deadline/launch>",
regex: /\bwhen(?:'s|s|\s+is|\s+was|\s+will|'ll|ll)\s+(?:the\s+)?(?:next\s+)?(?:\S+\s+){0,2}(final|game|match|event|conference|keynote|election|deadline|launch|webinar|meeting|hearing|summit|eclipses?|season|episode|release|version)\b/i,
},
],
});
// ─────────────────────────────────────────────────────────────────────────────
// 10. Elections & Results Group
// ─────────────────────────────────────────────────────────────────────────────
export const electionsResultsRuleGroup = new TimeRuleGroup({
category: "Elections & Results",
terms: [
"election result",
"election results",
"who won",
"who's winning",
"whos winning",
"leading now",
"who's leading",
"whos leading",
"leading in the election",
"projected winner",
"exit poll",
],
patterns: [
{
description: "<election/poll/vote> <results/winner/leading/status>",
regex: /\b(election|poll|vote)\s+(results?|winner|leading|status)\b/i,
},
{
description: "who is / who's / whos / who was / who will winning or leading or win",
regex: /\bwho(?:'s|s|\s+is|\s+was|\s+will|'ll|ll)\s+(?:currently\s+)?(win|winning|lead|leading|won)\b/i,
},
],
});
// ─────────────────────────────────────────────────────────────────────────────
// 11. Office Holders Group (Pattern-matched)
// Data list of role titles dynamically wrapped in query intent patterns
// ─────────────────────────────────────────────────────────────────────────────
export const officeHolderRoles: readonly string[] = [
"chief of defence staff",
"cds",
"chief of army staff",
"coas",
"chief of naval staff",
"cns",
"chief of air staff",
"cas",
"national security advisor",
"nsa",
"attorney general",
"solicitor general",
"prime minister",
"pm of",
"chief minister",
"cm of",
"cm",
"home minister",
"foreign minister",
"finance minister",
"defense minister",
"law minister",
"cabinet minister",
"minister of",
"president of",
"president",
"vice president",
"chief justice",
"governor of",
"governor",
"ceo",
"cto",
"cfo",
"mayor",
"captain",
"coach",
"chancellor",
"monarch",
"king",
"queen",
"head of state",
"chairman",
"secretary general",
"speaker of the house",
];
const sortedOfficeHolderRoles = [...officeHolderRoles].sort((a, b) => b.length - a.length);
const rolesSubPattern = sortedOfficeHolderRoles.map(r => escapeRegExp(r)).join("|");
export const officeHoldersRuleGroup = new TimeRuleGroup({
category: "Office Holders",
patterns: [
{
description: "Who is the <office holder> of ...",
regex: new RegExp(`\\bwho(?:'s|s|\\s+is)\\s+(?:the\\s+)?(?:current\\s+)?(${rolesSubPattern})\\b`, "i"),
},
{
description: "Current <office holder> of ...",
regex: new RegExp(`\\bcurrent\\s+(${rolesSubPattern})\\b`, "i"),
},
],
});
/**
* Ordered list of rule groups evaluated sequentially by expected real-world query frequency.
*/
export const timeDependencyRuleGroups: readonly TimeRuleGroup[] = [
relativeTimeRuleGroup,
recencyRuleGroup,
serviceOutagesRuleGroup,
weatherRuleGroup,
businessHoursAvailabilityRuleGroup,
liveScoresSportsRuleGroup,
financialMarketsRuleGroup,
transportationRuleGroup,
eventStatusRuleGroup,
electionsResultsRuleGroup,
officeHoldersRuleGroup,
];
export const DETECTOR_VERSION = "1.5";
export interface TimeDependencyOptions {
requestId?: string;
totalRequestTimeMs?: number;
}
/**
* Evaluates whether a query is a conceptual, educational, or descriptive question structure
* using indefinite noun phrases (e.g., "how does a recent update work?", "what's a new version in semantic versioning?").
*/
export function isConceptualOrDescriptiveQuery(text: string): boolean {
if (!text || typeof text !== "string") {
return false;
}
const conceptualPatterns = [
/\bhow\s+does\s+(?:a|an)\s+.+\s+works?\b/i,
/\bhow\s+do\s+(?:a|an)\s+.+\s+work\b/i,
/\bhow(?:'s|s|\s+is)\s+(?:a|an)\s+.+\s+works?\b/i,
/\bhow\s+to\s+(?:build|create|write|implement|design|develop|make|construct|install|configure|set\s+up|use)\s+(?:a|an)\b/i,
/\bwhat(?:'s|s|\s+is|\s+defines|\s+constitutes)\s+(?:a|an)\b/i,
/\bexplain\s+(?:how|the\s+concept\s+of|what(?:'s|s|\s+is)?\s+(?:a|an)?)\b/i,
/\b(?:definition|meaning)\s+of\s+(?:a|an)\b/i,
];
return conceptualPatterns.some(pattern => pattern.test(text));
}
/**
* Evaluates user prompt text against all time dependency rule groups.
* Returns true if any rule group matches.
*/
export function requiresCurrentTimeContext(
userText: string,
options?: string | TimeDependencyOptions
): boolean {
if (!userText || typeof userText !== "string") {
return false;
}
if (isConceptualOrDescriptiveQuery(userText)) {
return false;
}
return timeDependencyRuleGroups.some(rg => rg.matches(userText));
}