@useody/detectors 0.0.1
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/LICENSE +198 -0
- package/README.md +42 -0
- package/dist/claim-comparison.d.ts +12 -0
- package/dist/claim-comparison.d.ts.map +1 -0
- package/dist/claim-comparison.js +108 -0
- package/dist/claim-nli.d.ts +15 -0
- package/dist/claim-nli.d.ts.map +1 -0
- package/dist/claim-nli.js +181 -0
- package/dist/consensus.d.ts +26 -0
- package/dist/consensus.d.ts.map +1 -0
- package/dist/consensus.js +211 -0
- package/dist/consultant-analysis.d.ts +20 -0
- package/dist/consultant-analysis.d.ts.map +1 -0
- package/dist/consultant-analysis.js +69 -0
- package/dist/consultant-prompts.d.ts +83 -0
- package/dist/consultant-prompts.d.ts.map +1 -0
- package/dist/consultant-prompts.js +135 -0
- package/dist/contradiction-helpers.d.ts +40 -0
- package/dist/contradiction-helpers.d.ts.map +1 -0
- package/dist/contradiction-helpers.js +163 -0
- package/dist/contradictions.d.ts +20 -0
- package/dist/contradictions.d.ts.map +1 -0
- package/dist/contradictions.js +235 -0
- package/dist/duplicates.d.ts +14 -0
- package/dist/duplicates.d.ts.map +1 -0
- package/dist/duplicates.js +95 -0
- package/dist/health-score.d.ts +13 -0
- package/dist/health-score.d.ts.map +1 -0
- package/dist/health-score.js +53 -0
- package/dist/helpers/index.d.ts +7 -0
- package/dist/helpers/index.d.ts.map +1 -0
- package/dist/helpers/index.js +7 -0
- package/dist/helpers/llm-timeout.d.ts +14 -0
- package/dist/helpers/llm-timeout.d.ts.map +1 -0
- package/dist/helpers/llm-timeout.js +30 -0
- package/dist/helpers/text-utils.d.ts +21 -0
- package/dist/helpers/text-utils.d.ts.map +1 -0
- package/dist/helpers/text-utils.js +63 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/run-detection.d.ts +29 -0
- package/dist/run-detection.d.ts.map +1 -0
- package/dist/run-detection.js +46 -0
- package/dist/staleness.d.ts +21 -0
- package/dist/staleness.d.ts.map +1 -0
- package/dist/staleness.js +128 -0
- package/dist/time-bomb-utils.d.ts +23 -0
- package/dist/time-bomb-utils.d.ts.map +1 -0
- package/dist/time-bomb-utils.js +161 -0
- package/dist/time-bombs.d.ts +14 -0
- package/dist/time-bombs.d.ts.map +1 -0
- package/dist/time-bombs.js +113 -0
- package/dist/undocumented.d.ts +13 -0
- package/dist/undocumented.d.ts.map +1 -0
- package/dist/undocumented.js +84 -0
- package/package.json +48 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const DEFAULT_TIMEOUT_MS = 15_000;
|
|
2
|
+
/** Check whether an error is an unrecoverable auth/quota failure. */
|
|
3
|
+
function isFatalLlmError(err) {
|
|
4
|
+
if (!(err instanceof Error))
|
|
5
|
+
return false;
|
|
6
|
+
return err.name === 'LLMAuthError'
|
|
7
|
+
|| err.message.includes('authentication/quota error')
|
|
8
|
+
|| err.message.includes('Invalid API key');
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Call LLM with a timeout.
|
|
12
|
+
* Returns empty string on transient failures (timeout, 5xx).
|
|
13
|
+
* Re-throws fatal errors (401, 403) so the scan can fail fast.
|
|
14
|
+
*/
|
|
15
|
+
export async function completeWithTimeout(llm, messages, options, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
16
|
+
try {
|
|
17
|
+
return await Promise.race([
|
|
18
|
+
llm.complete(messages, options),
|
|
19
|
+
new Promise((_, reject) => {
|
|
20
|
+
setTimeout(() => reject(new Error('LLM request timed out')), timeoutMs);
|
|
21
|
+
}),
|
|
22
|
+
]);
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
if (isFatalLlmError(err))
|
|
26
|
+
throw err;
|
|
27
|
+
return '';
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=llm-timeout.js.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text utility functions for lexical matching in detectors.
|
|
3
|
+
* Extracted from the undocumented detector.
|
|
4
|
+
* @module helpers/text-utils
|
|
5
|
+
*/
|
|
6
|
+
/** Stop words filtered out during tokenization. */
|
|
7
|
+
export declare const STOP_WORDS: Set<string>;
|
|
8
|
+
/** Lowercase, remove punctuation, collapse whitespace. */
|
|
9
|
+
export declare function normalize(text: string): string;
|
|
10
|
+
/** Normalize, split, and filter stopwords + short tokens (len < 2). */
|
|
11
|
+
export declare function tokenize(text: string): string[];
|
|
12
|
+
/** Return the intersection of two string arrays. */
|
|
13
|
+
export declare function sharedTokens(a: string[], b: string[]): string[];
|
|
14
|
+
/** Jaccard similarity: intersection / union. Returns 0 for empty inputs. */
|
|
15
|
+
export declare function lexicalScore(a: string[], b: string[]): number;
|
|
16
|
+
/** Build a signal from a title and facts for lexical matching. */
|
|
17
|
+
export declare function buildSignal(title: string, facts: string[]): {
|
|
18
|
+
titleTokens: string[];
|
|
19
|
+
tokens: string[];
|
|
20
|
+
};
|
|
21
|
+
//# sourceMappingURL=text-utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"text-utils.d.ts","sourceRoot":"","sources":["../../src/helpers/text-utils.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,mDAAmD;AACnD,eAAO,MAAM,UAAU,aAoBrB,CAAC;AAEH,0DAA0D;AAC1D,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAM9C;AAED,uEAAuE;AACvE,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAI/C;AAED,oDAAoD;AACpD,wBAAgB,YAAY,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAG/D;AAED,4EAA4E;AAC5E,wBAAgB,YAAY,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,MAAM,EAAE,GAAG,MAAM,CAM7D;AAED,kEAAkE;AAClE,wBAAgB,WAAW,CACzB,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,EAAE,GACd;IAAE,WAAW,EAAE,MAAM,EAAE,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,CAK7C"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text utility functions for lexical matching in detectors.
|
|
3
|
+
* Extracted from the undocumented detector.
|
|
4
|
+
* @module helpers/text-utils
|
|
5
|
+
*/
|
|
6
|
+
/** Stop words filtered out during tokenization. */
|
|
7
|
+
export const STOP_WORDS = new Set([
|
|
8
|
+
'the',
|
|
9
|
+
'and',
|
|
10
|
+
'for',
|
|
11
|
+
'with',
|
|
12
|
+
'from',
|
|
13
|
+
'this',
|
|
14
|
+
'that',
|
|
15
|
+
'discussion',
|
|
16
|
+
'update',
|
|
17
|
+
'changes',
|
|
18
|
+
'change',
|
|
19
|
+
'proposal',
|
|
20
|
+
'announcement',
|
|
21
|
+
'rollout',
|
|
22
|
+
'thread',
|
|
23
|
+
'notes',
|
|
24
|
+
'week',
|
|
25
|
+
'month',
|
|
26
|
+
'new',
|
|
27
|
+
]);
|
|
28
|
+
/** Lowercase, remove punctuation, collapse whitespace. */
|
|
29
|
+
export function normalize(text) {
|
|
30
|
+
return text
|
|
31
|
+
.toLowerCase()
|
|
32
|
+
.replace(/[^a-z0-9\s]+/g, ' ')
|
|
33
|
+
.replace(/\s+/g, ' ')
|
|
34
|
+
.trim();
|
|
35
|
+
}
|
|
36
|
+
/** Normalize, split, and filter stopwords + short tokens (len < 2). */
|
|
37
|
+
export function tokenize(text) {
|
|
38
|
+
return normalize(text)
|
|
39
|
+
.split(' ')
|
|
40
|
+
.filter((token) => token.length >= 2 && !STOP_WORDS.has(token));
|
|
41
|
+
}
|
|
42
|
+
/** Return the intersection of two string arrays. */
|
|
43
|
+
export function sharedTokens(a, b) {
|
|
44
|
+
const bSet = new Set(b);
|
|
45
|
+
return [...new Set(a)].filter((token) => bSet.has(token));
|
|
46
|
+
}
|
|
47
|
+
/** Jaccard similarity: intersection / union. Returns 0 for empty inputs. */
|
|
48
|
+
export function lexicalScore(a, b) {
|
|
49
|
+
const aSet = new Set(a);
|
|
50
|
+
const bSet = new Set(b);
|
|
51
|
+
const union = new Set([...aSet, ...bSet]);
|
|
52
|
+
if (union.size === 0)
|
|
53
|
+
return 0;
|
|
54
|
+
return [...aSet].filter((token) => bSet.has(token)).length / union.size;
|
|
55
|
+
}
|
|
56
|
+
/** Build a signal from a title and facts for lexical matching. */
|
|
57
|
+
export function buildSignal(title, facts) {
|
|
58
|
+
return {
|
|
59
|
+
titleTokens: tokenize(title),
|
|
60
|
+
tokens: tokenize(`${title} ${facts.slice(0, 5).join(' ')}`),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=text-utils.js.map
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @ody/detectors — pure function detectors for knowledge graph analysis.
|
|
3
|
+
* @module @ody/detectors
|
|
4
|
+
*/
|
|
5
|
+
export { detectContradictions } from './contradictions.js';
|
|
6
|
+
export { detectDuplicates } from './duplicates.js';
|
|
7
|
+
export { detectStaleness } from './staleness.js';
|
|
8
|
+
export { detectUndocumented } from './undocumented.js';
|
|
9
|
+
export { detectTimeBombs } from './time-bombs.js';
|
|
10
|
+
export { runDetection } from './run-detection.js';
|
|
11
|
+
export type { RunDetectionInput, RunDetectionOutput, DetectorRunStats, } from './run-detection.js';
|
|
12
|
+
export { analyzeCorpus } from './consultant-analysis.js';
|
|
13
|
+
export { runConsensusAnalysis } from './consensus.js';
|
|
14
|
+
export type { ConsensusOptions } from './consensus.js';
|
|
15
|
+
export { computeDeterministicHealthScore } from './health-score.js';
|
|
16
|
+
export type { FindingCategory, ConsultingFinding, HealthScore, DocumentInfo, AnalysisResult, AnalysisInput, } from './consultant-analysis.js';
|
|
17
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,YAAY,EACV,iBAAiB,EACjB,kBAAkB,EAClB,gBAAgB,GACjB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AACzD,OAAO,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AACtD,YAAY,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AACvD,OAAO,EAAE,+BAA+B,EAAE,MAAM,mBAAmB,CAAC;AACpE,YAAY,EACV,eAAe,EACf,iBAAiB,EACjB,WAAW,EACX,YAAY,EACZ,cAAc,EACd,aAAa,GACd,MAAM,0BAA0B,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @ody/detectors — pure function detectors for knowledge graph analysis.
|
|
3
|
+
* @module @ody/detectors
|
|
4
|
+
*/
|
|
5
|
+
export { detectContradictions } from './contradictions.js';
|
|
6
|
+
export { detectDuplicates } from './duplicates.js';
|
|
7
|
+
export { detectStaleness } from './staleness.js';
|
|
8
|
+
export { detectUndocumented } from './undocumented.js';
|
|
9
|
+
export { detectTimeBombs } from './time-bombs.js';
|
|
10
|
+
export { runDetection } from './run-detection.js';
|
|
11
|
+
export { analyzeCorpus } from './consultant-analysis.js';
|
|
12
|
+
export { runConsensusAnalysis } from './consensus.js';
|
|
13
|
+
export { computeDeterministicHealthScore } from './health-score.js';
|
|
14
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple detection orchestrator — runs all five detectors on in-memory arrays.
|
|
3
|
+
* Use this for library / adapter integration where nodes and edges are
|
|
4
|
+
* already loaded (no repository or vector index required).
|
|
5
|
+
* @module run-detection
|
|
6
|
+
*/
|
|
7
|
+
import type { Detection, DetectorFn, KnowledgeEdge, KnowledgeNode, LLMProvider } from '@useody/platform-core';
|
|
8
|
+
export interface RunDetectionInput {
|
|
9
|
+
nodes: KnowledgeNode[];
|
|
10
|
+
edges: KnowledgeEdge[];
|
|
11
|
+
llm?: LLMProvider;
|
|
12
|
+
detectors?: DetectorFn[];
|
|
13
|
+
onProgress?: (name: string, status: 'started' | 'completed') => void;
|
|
14
|
+
}
|
|
15
|
+
export interface DetectorRunStats {
|
|
16
|
+
name: string;
|
|
17
|
+
detectionCount: number;
|
|
18
|
+
durationMs: number;
|
|
19
|
+
}
|
|
20
|
+
export interface RunDetectionOutput {
|
|
21
|
+
detections: Detection[];
|
|
22
|
+
stats: DetectorRunStats[];
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Run detection across in-memory knowledge nodes and edges.
|
|
26
|
+
* By default runs all five detectors; pass `detectors` to override.
|
|
27
|
+
*/
|
|
28
|
+
export declare function runDetection(input: RunDetectionInput): Promise<RunDetectionOutput>;
|
|
29
|
+
//# sourceMappingURL=run-detection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-detection.d.ts","sourceRoot":"","sources":["../src/run-detection.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,EACV,SAAS,EACT,UAAU,EACV,aAAa,EACb,aAAa,EACb,WAAW,EACZ,MAAM,uBAAuB,CAAC;AAO/B,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,aAAa,EAAE,CAAC;IACvB,KAAK,EAAE,aAAa,EAAE,CAAC;IACvB,GAAG,CAAC,EAAE,WAAW,CAAC;IAClB,SAAS,CAAC,EAAE,UAAU,EAAE,CAAC;IACzB,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,GAAG,WAAW,KAAK,IAAI,CAAC;CACtE;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,cAAc,EAAE,MAAM,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,kBAAkB;IACjC,UAAU,EAAE,SAAS,EAAE,CAAC;IACxB,KAAK,EAAE,gBAAgB,EAAE,CAAC;CAC3B;AAUD;;;GAGG;AACH,wBAAsB,YAAY,CAChC,KAAK,EAAE,iBAAiB,GACvB,OAAO,CAAC,kBAAkB,CAAC,CA6B7B"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { detectContradictions } from './contradictions.js';
|
|
2
|
+
import { detectDuplicates } from './duplicates.js';
|
|
3
|
+
import { detectStaleness } from './staleness.js';
|
|
4
|
+
import { detectUndocumented } from './undocumented.js';
|
|
5
|
+
import { detectTimeBombs } from './time-bombs.js';
|
|
6
|
+
const DEFAULT_DETECTORS = [
|
|
7
|
+
detectContradictions,
|
|
8
|
+
detectDuplicates,
|
|
9
|
+
detectStaleness,
|
|
10
|
+
detectUndocumented,
|
|
11
|
+
detectTimeBombs,
|
|
12
|
+
];
|
|
13
|
+
/**
|
|
14
|
+
* Run detection across in-memory knowledge nodes and edges.
|
|
15
|
+
* By default runs all five detectors; pass `detectors` to override.
|
|
16
|
+
*/
|
|
17
|
+
export async function runDetection(input) {
|
|
18
|
+
const { nodes, edges, llm, onProgress } = input;
|
|
19
|
+
const detectors = input.detectors ?? DEFAULT_DETECTORS;
|
|
20
|
+
const allDetections = [];
|
|
21
|
+
const stats = [];
|
|
22
|
+
for (const detector of detectors) {
|
|
23
|
+
const name = detector.name || 'unknown';
|
|
24
|
+
onProgress?.(name, 'started');
|
|
25
|
+
const start = Date.now();
|
|
26
|
+
try {
|
|
27
|
+
const detections = await detector(nodes, edges, llm);
|
|
28
|
+
const durationMs = Date.now() - start;
|
|
29
|
+
allDetections.push(...detections);
|
|
30
|
+
stats.push({ name, detectionCount: detections.length, durationMs });
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
const durationMs = Date.now() - start;
|
|
34
|
+
stats.push({ name, detectionCount: 0, durationMs });
|
|
35
|
+
// Re-throw fatal errors (auth/quota); skip transient ones
|
|
36
|
+
if (err instanceof Error && (err.name === 'LLMAuthError'
|
|
37
|
+
|| err.message.includes('authentication/quota error'))) {
|
|
38
|
+
throw err;
|
|
39
|
+
}
|
|
40
|
+
// Transient error — continue with remaining detectors
|
|
41
|
+
}
|
|
42
|
+
onProgress?.(name, 'completed');
|
|
43
|
+
}
|
|
44
|
+
return { detections: allDetections, stats };
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=run-detection.js.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Staleness detector.
|
|
3
|
+
* Finds knowledge nodes superseded by newer nodes or with stale date references.
|
|
4
|
+
* @module staleness
|
|
5
|
+
*/
|
|
6
|
+
import type { DetectorFn } from '@useody/platform-core';
|
|
7
|
+
declare const SIX_MONTHS_MS: number;
|
|
8
|
+
/** Parse a date string like "January 2025" or "2025-01" into a Date. */
|
|
9
|
+
declare function parseDateRef(ref: string): Date | null;
|
|
10
|
+
/** Extract referenced dates from content. */
|
|
11
|
+
declare function extractContentDates(text: string): {
|
|
12
|
+
label: string;
|
|
13
|
+
date: Date;
|
|
14
|
+
}[];
|
|
15
|
+
/**
|
|
16
|
+
* Detect stale knowledge nodes.
|
|
17
|
+
* Uses supersedes edges and date-based content heuristics.
|
|
18
|
+
*/
|
|
19
|
+
declare const detectStaleness: DetectorFn;
|
|
20
|
+
export { detectStaleness, extractContentDates, parseDateRef, SIX_MONTHS_MS };
|
|
21
|
+
//# sourceMappingURL=staleness.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"staleness.d.ts","sourceRoot":"","sources":["../src/staleness.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EAIV,UAAU,EAEX,MAAM,uBAAuB,CAAC;AAc/B,QAAA,MAAM,aAAa,QAA+B,CAAC;AAEnD,wEAAwE;AACxE,iBAAS,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAe9C;AAED,6CAA6C;AAC7C,iBAAS,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,IAAI,CAAA;CAAE,EAAE,CAY1E;AAED;;;GAGG;AACH,QAAA,MAAM,eAAe,EAAE,UAyFtB,CAAC;AAOF,OAAO,EAAE,eAAe,EAAE,mBAAmB,EAAE,YAAY,EAAE,aAAa,EAAE,CAAC"}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
const LAST_UPDATED_PATTERN = /\b(?:last\s+updated|updated|modified)\s*:?\s*((?:january|february|march|april|may|june|july|august|september|october|november|december)\s+\d{4}|\d{4}-\d{2}(?:-\d{2})?)\b/gi;
|
|
2
|
+
const AS_OF_PATTERN = /\bas\s+of\s+((?:january|february|march|april|may|june|july|august|september|october|november|december)\s+\d{4}|\d{4}-\d{2}(?:-\d{2})?)\b/gi;
|
|
3
|
+
const MONTH_MAP = {
|
|
4
|
+
january: 0, february: 1, march: 2, april: 3,
|
|
5
|
+
may: 4, june: 5, july: 6, august: 7,
|
|
6
|
+
september: 8, october: 9, november: 10, december: 11,
|
|
7
|
+
};
|
|
8
|
+
const SIX_MONTHS_MS = 6 * 30 * 24 * 60 * 60 * 1000;
|
|
9
|
+
/** Parse a date string like "January 2025" or "2025-01" into a Date. */
|
|
10
|
+
function parseDateRef(ref) {
|
|
11
|
+
const isoMatch = ref.match(/^(\d{4})-(\d{2})(?:-(\d{2}))?$/);
|
|
12
|
+
if (isoMatch) {
|
|
13
|
+
const y = parseInt(isoMatch[1], 10);
|
|
14
|
+
const m = parseInt(isoMatch[2], 10) - 1;
|
|
15
|
+
const d = isoMatch[3] ? parseInt(isoMatch[3], 10) : 1;
|
|
16
|
+
return new Date(y, m, d);
|
|
17
|
+
}
|
|
18
|
+
const monthMatch = ref.match(/^(january|february|march|april|may|june|july|august|september|october|november|december)\s+(\d{4})$/i);
|
|
19
|
+
if (monthMatch) {
|
|
20
|
+
const m = MONTH_MAP[monthMatch[1].toLowerCase()];
|
|
21
|
+
const y = parseInt(monthMatch[2], 10);
|
|
22
|
+
return new Date(y, m, 1);
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
/** Extract referenced dates from content. */
|
|
27
|
+
function extractContentDates(text) {
|
|
28
|
+
const results = [];
|
|
29
|
+
for (const pattern of [LAST_UPDATED_PATTERN, AS_OF_PATTERN]) {
|
|
30
|
+
const re = new RegExp(pattern.source, pattern.flags);
|
|
31
|
+
let m;
|
|
32
|
+
while ((m = re.exec(text)) !== null) {
|
|
33
|
+
const parsed = parseDateRef(m[1]);
|
|
34
|
+
if (parsed)
|
|
35
|
+
results.push({ label: m[0], date: parsed });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return results;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Detect stale knowledge nodes.
|
|
42
|
+
* Uses supersedes edges and date-based content heuristics.
|
|
43
|
+
*/
|
|
44
|
+
const detectStaleness = async (nodes, edges, _llm) => {
|
|
45
|
+
const detections = [];
|
|
46
|
+
const now = new Date();
|
|
47
|
+
const nodeMap = new Map(nodes.map((n) => [n.id, n]));
|
|
48
|
+
// Edge-based detection (existing)
|
|
49
|
+
const supersedesEdges = edges.filter((e) => e.type === 'supersedes');
|
|
50
|
+
for (const edge of supersedesEdges) {
|
|
51
|
+
const newer = nodeMap.get(edge.sourceId);
|
|
52
|
+
const older = nodeMap.get(edge.targetId);
|
|
53
|
+
if (!newer || !older)
|
|
54
|
+
continue;
|
|
55
|
+
const edgeCreated = edge.createdAt ?? new Date(0);
|
|
56
|
+
const olderLastModified = older.content.source?.lastModified ?? older.updatedAt;
|
|
57
|
+
if (olderLastModified >= edgeCreated)
|
|
58
|
+
continue;
|
|
59
|
+
const olderFile = older.content.source?.sourceId?.split('/').pop() ?? older.title;
|
|
60
|
+
const newerFile = newer.content.source?.sourceId?.split('/').pop() ?? newer.title;
|
|
61
|
+
detections.push({
|
|
62
|
+
type: 'staleness',
|
|
63
|
+
severity: 'warning',
|
|
64
|
+
nodeIds: [older.id, newer.id],
|
|
65
|
+
description: `"${older.title}" may be outdated — a newer version exists in "${newer.title}".`,
|
|
66
|
+
suggestedAction: `Review ${olderFile} and update it to match ${newerFile}, or mark it as superseded.`,
|
|
67
|
+
metadata: {
|
|
68
|
+
claimA: `Older: ${older.title} (${olderFile})`,
|
|
69
|
+
claimB: `Newer: ${newer.title} (${newerFile})`,
|
|
70
|
+
topic: 'version drift',
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
// Date-based heuristic: content referencing old dates
|
|
75
|
+
const superseededIds = new Set(supersedesEdges.map((e) => e.targetId));
|
|
76
|
+
for (const node of nodes) {
|
|
77
|
+
if (superseededIds.has(node.id))
|
|
78
|
+
continue; // already caught above
|
|
79
|
+
const raw = node.content.raw ?? '';
|
|
80
|
+
const text = `${node.title} ${node.content.summary} ${raw}`;
|
|
81
|
+
const refs = extractContentDates(text);
|
|
82
|
+
for (const ref of refs) {
|
|
83
|
+
const age = now.getTime() - ref.date.getTime();
|
|
84
|
+
if (age > SIX_MONTHS_MS) {
|
|
85
|
+
const monthsOld = Math.round(age / (30 * 24 * 60 * 60 * 1000));
|
|
86
|
+
const nodeFile = node.content.source?.sourceId?.split('/').pop() ?? node.title;
|
|
87
|
+
detections.push({
|
|
88
|
+
type: 'staleness',
|
|
89
|
+
severity: 'info',
|
|
90
|
+
nodeIds: [node.id],
|
|
91
|
+
description: `"${node.title}" references "${ref.label}" (${monthsOld} months ago) — content may be outdated.`,
|
|
92
|
+
suggestedAction: `Review ${nodeFile} and verify the information is still current.`,
|
|
93
|
+
});
|
|
94
|
+
break; // one detection per node
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// Deduplicate: group edge-based detections by older node
|
|
99
|
+
const byOlder = new Map();
|
|
100
|
+
const dateDetections = [];
|
|
101
|
+
for (const det of detections) {
|
|
102
|
+
if (det.type === 'staleness' && det.severity === 'warning' && det.nodeIds.length >= 2) {
|
|
103
|
+
const key = det.nodeIds[0];
|
|
104
|
+
const existing = byOlder.get(key);
|
|
105
|
+
if (existing) {
|
|
106
|
+
const merged = [...new Set([...existing.nodeIds, ...det.nodeIds])];
|
|
107
|
+
byOlder.set(key, {
|
|
108
|
+
...existing,
|
|
109
|
+
nodeIds: merged,
|
|
110
|
+
description: `${existing.description.split('.')[0]}. (${String(merged.length - 1)} newer versions found)`,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
byOlder.set(key, det);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
dateDetections.push(det);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return [...byOlder.values(), ...dateDetections];
|
|
122
|
+
};
|
|
123
|
+
detectStaleness.preFilter = {
|
|
124
|
+
similarityThreshold: 0.5,
|
|
125
|
+
topK: 15,
|
|
126
|
+
};
|
|
127
|
+
export { detectStaleness, extractContentDates, parseDateRef, SIX_MONTHS_MS };
|
|
128
|
+
//# sourceMappingURL=staleness.js.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Date pattern parsing and deadline classification utilities for time bomb detection.
|
|
3
|
+
* Extracted from time-bombs.ts to keep files under 250 lines.
|
|
4
|
+
* @module time-bomb-utils
|
|
5
|
+
*/
|
|
6
|
+
import type { KnowledgeNode, Detection } from '@useody/platform-core';
|
|
7
|
+
export declare const DATE_PATTERN: RegExp;
|
|
8
|
+
export declare const QUARTER_PATTERN: RegExp;
|
|
9
|
+
export declare const YYYY_MM_PATTERN: RegExp;
|
|
10
|
+
export declare const BY_MONTH_YEAR: RegExp;
|
|
11
|
+
export declare const BY_END_OF: RegExp;
|
|
12
|
+
/** Parsed deadline result. */
|
|
13
|
+
export interface ParsedDeadline {
|
|
14
|
+
label: string;
|
|
15
|
+
endDate: Date;
|
|
16
|
+
}
|
|
17
|
+
/** Extract deadline dates from text using regex patterns. */
|
|
18
|
+
export declare function extractDeadlines(text: string, now: Date): ParsedDeadline[];
|
|
19
|
+
/** Classify deadline relative to now. */
|
|
20
|
+
export declare function classifyDeadline(deadline: ParsedDeadline, now: Date, nodeId: string, _nowLabel: string, docType?: string): Detection | null;
|
|
21
|
+
/** Find a node that may have completed the deadline referenced by another node. */
|
|
22
|
+
export declare function findCompletionNode(nodeId: string, nodeText: string, nodes: KnowledgeNode[]): string | undefined;
|
|
23
|
+
//# sourceMappingURL=time-bomb-utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"time-bomb-utils.d.ts","sourceRoot":"","sources":["../src/time-bomb-utils.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAEtE,eAAO,MAAM,YAAY,QACwM,CAAC;AAElO,eAAO,MAAM,eAAe,QAA+B,CAAC;AAC5D,eAAO,MAAM,eAAe,QAA2B,CAAC;AACxD,eAAO,MAAM,aAAa,QACwG,CAAC;AACnI,eAAO,MAAM,SAAS,QACqB,CAAC;AAgD5C,8BAA8B;AAC9B,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,IAAI,CAAC;CACf;AAED,6DAA6D;AAC7D,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,GAAG,cAAc,EAAE,CA6C1E;AAED,yCAAyC;AACzC,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,cAAc,EACxB,GAAG,EAAE,IAAI,EACT,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,OAAO,CAAC,EAAE,MAAM,GACf,SAAS,GAAG,IAAI,CAkClB;AAiBD,mFAAmF;AACnF,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,EAAE,GAAG,MAAM,GAAG,SAAS,CAgB/G"}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
export const DATE_PATTERN = /\b(Q[1-4]\s*20\d{2}|20\d{2}-\d{2}|by\s+(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec|january|february|march|april|may|june|july|august|september|october|november|december)\b|deadline|due\s+date|expires?|by\s+end\s+of)/i;
|
|
2
|
+
export const QUARTER_PATTERN = /\bQ([1-4])\s*(20\d{2})\b/gi;
|
|
3
|
+
export const YYYY_MM_PATTERN = /\b(20\d{2})-(\d{2})\b/g;
|
|
4
|
+
export const BY_MONTH_YEAR = /\bby\s+(?:end\s+of\s+)?(january|february|march|april|may|june|july|august|september|october|november|december)\s+(20\d{2})\b/gi;
|
|
5
|
+
export const BY_END_OF = /\bby\s+(?:end\s+of\s+)?(month|year)\b/gi;
|
|
6
|
+
const MONTH_MAP = {
|
|
7
|
+
january: 0, february: 1, march: 2, april: 3,
|
|
8
|
+
may: 4, june: 5, july: 6, august: 7,
|
|
9
|
+
september: 8, october: 9, november: 10, december: 11,
|
|
10
|
+
};
|
|
11
|
+
const QUARTER_END_MONTH = [2, 5, 8, 11]; // Q1=Mar, Q2=Jun, Q3=Sep, Q4=Dec
|
|
12
|
+
const COMPLETION_RE = /\b(completed|was\s+done|shipped|resolved|closed|merged|finished|delivered|launched|migrated|moved\s+from)\b/i;
|
|
13
|
+
/** Check if a match position follows a document metadata label. */
|
|
14
|
+
function isMetadataDate(text, index) {
|
|
15
|
+
const before = text.slice(Math.max(0, index - 50), index).replace(/\*+/g, '');
|
|
16
|
+
return /(?:last\s+updated|last\s+modified|updated|date|version|consolidated|created|published|generated)\s*:?\s*$/i.test(before.trimEnd());
|
|
17
|
+
}
|
|
18
|
+
/** Deadline-framing words near a YYYY-MM date. */
|
|
19
|
+
const DEADLINE_CONTEXT_RE = /\b(deadline|due|by|expires?|target|complete|deliver|launch|ship|release|finish|submit|migrate|renewal|until|before|review)\b/i;
|
|
20
|
+
/** Check if the sentence containing a YYYY-MM match has deadline-framing language. */
|
|
21
|
+
function hasDeadlineContext(text, index) {
|
|
22
|
+
let start = index;
|
|
23
|
+
while (start > 0 && !/[.!?\n]/.test(text[start - 1]))
|
|
24
|
+
start--;
|
|
25
|
+
let end = index;
|
|
26
|
+
while (end < text.length && !/[.!?\n]/.test(text[end]))
|
|
27
|
+
end++;
|
|
28
|
+
const sent = text.slice(start, end);
|
|
29
|
+
return DEADLINE_CONTEXT_RE.test(sent);
|
|
30
|
+
}
|
|
31
|
+
/** Check if the sentence containing index already signals a completed past event. */
|
|
32
|
+
function sentenceIsCompleted(text, index) {
|
|
33
|
+
let start = index;
|
|
34
|
+
while (start > 0 && !/[.!?\n]/.test(text[start - 1]))
|
|
35
|
+
start--;
|
|
36
|
+
let end = index;
|
|
37
|
+
while (end < text.length && !/[.!?\n]/.test(text[end]))
|
|
38
|
+
end++;
|
|
39
|
+
const sent = text.slice(start, end);
|
|
40
|
+
return COMPLETION_RE.test(sent);
|
|
41
|
+
}
|
|
42
|
+
/** Check if the match is inside a markdown table cell. */
|
|
43
|
+
function isInTableCell(text, index) {
|
|
44
|
+
let lineStart = index;
|
|
45
|
+
while (lineStart > 0 && text[lineStart - 1] !== '\n')
|
|
46
|
+
lineStart--;
|
|
47
|
+
return text.slice(lineStart, index).trimStart().startsWith('|');
|
|
48
|
+
}
|
|
49
|
+
/** Extract deadline dates from text using regex patterns. */
|
|
50
|
+
export function extractDeadlines(text, now) {
|
|
51
|
+
const results = [];
|
|
52
|
+
let match;
|
|
53
|
+
const qp = new RegExp(QUARTER_PATTERN.source, QUARTER_PATTERN.flags);
|
|
54
|
+
while ((match = qp.exec(text)) !== null) {
|
|
55
|
+
if (isInTableCell(text, match.index))
|
|
56
|
+
continue;
|
|
57
|
+
if (sentenceIsCompleted(text, match.index))
|
|
58
|
+
continue;
|
|
59
|
+
const q = parseInt(match[1], 10);
|
|
60
|
+
const year = parseInt(match[2], 10);
|
|
61
|
+
const endMonth = QUARTER_END_MONTH[q - 1];
|
|
62
|
+
const endDate = new Date(year, endMonth + 1, 0, 23, 59, 59);
|
|
63
|
+
results.push({ label: match[0], endDate });
|
|
64
|
+
}
|
|
65
|
+
const ymp = new RegExp(YYYY_MM_PATTERN.source, YYYY_MM_PATTERN.flags);
|
|
66
|
+
while ((match = ymp.exec(text)) !== null) {
|
|
67
|
+
if (isInTableCell(text, match.index))
|
|
68
|
+
continue;
|
|
69
|
+
if (isMetadataDate(text, match.index))
|
|
70
|
+
continue;
|
|
71
|
+
if (sentenceIsCompleted(text, match.index))
|
|
72
|
+
continue;
|
|
73
|
+
if (!hasDeadlineContext(text, match.index))
|
|
74
|
+
continue;
|
|
75
|
+
const year = parseInt(match[1], 10);
|
|
76
|
+
const month = parseInt(match[2], 10) - 1;
|
|
77
|
+
const endDate = new Date(year, month + 1, 0, 23, 59, 59);
|
|
78
|
+
results.push({ label: match[0], endDate });
|
|
79
|
+
}
|
|
80
|
+
const bmy = new RegExp(BY_MONTH_YEAR.source, BY_MONTH_YEAR.flags);
|
|
81
|
+
while ((match = bmy.exec(text)) !== null) {
|
|
82
|
+
const month = MONTH_MAP[match[1].toLowerCase()];
|
|
83
|
+
const year = parseInt(match[2], 10);
|
|
84
|
+
const endDate = new Date(year, month + 1, 0, 23, 59, 59);
|
|
85
|
+
results.push({ label: match[0], endDate });
|
|
86
|
+
}
|
|
87
|
+
const beo = new RegExp(BY_END_OF.source, BY_END_OF.flags);
|
|
88
|
+
while ((match = beo.exec(text)) !== null) {
|
|
89
|
+
const unit = match[1].toLowerCase();
|
|
90
|
+
const endDate = unit === 'year'
|
|
91
|
+
? new Date(now.getFullYear(), 11, 31, 23, 59, 59)
|
|
92
|
+
: new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 59, 59);
|
|
93
|
+
results.push({ label: match[0], endDate });
|
|
94
|
+
}
|
|
95
|
+
return results;
|
|
96
|
+
}
|
|
97
|
+
/** Classify deadline relative to now. */
|
|
98
|
+
export function classifyDeadline(deadline, now, nodeId, _nowLabel, docType) {
|
|
99
|
+
const diff = deadline.endDate.getTime() - now.getTime();
|
|
100
|
+
const thirtyDays = 30 * 24 * 60 * 60 * 1000;
|
|
101
|
+
const isMeetingNote = docType === 'meeting_notes' || docType === 'changelog';
|
|
102
|
+
if (diff < 0) {
|
|
103
|
+
const msPerMonth = 30 * 24 * 60 * 60 * 1000;
|
|
104
|
+
const monthsAgo = Math.round(-diff / msPerMonth);
|
|
105
|
+
const agoStr = monthsAgo <= 0 ? 'recently' : `${monthsAgo} month${monthsAgo === 1 ? '' : 's'} ago`;
|
|
106
|
+
const severity = isMeetingNote ? 'info' : 'warning';
|
|
107
|
+
const noteCtx = isMeetingNote ? ' (mentioned in meeting notes — verify if completed)' : '';
|
|
108
|
+
return {
|
|
109
|
+
type: 'time_bomb', severity, nodeIds: [nodeId],
|
|
110
|
+
description: `Deadline "${deadline.label}" has passed (${agoStr}).${noteCtx}`,
|
|
111
|
+
suggestedAction: isMeetingNote
|
|
112
|
+
? `This deadline was mentioned in meeting notes. Check if it was completed in your task tracker.`
|
|
113
|
+
: `Deadline "${deadline.label}" has passed. Update or remove.`,
|
|
114
|
+
metadata: { deadline: deadline.label, expired: true, monthsAgo, docType },
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
if (diff <= thirtyDays) {
|
|
118
|
+
return {
|
|
119
|
+
type: 'time_bomb', severity: 'warning', nodeIds: [nodeId],
|
|
120
|
+
description: `Deadline approaching: "${deadline.label}".`,
|
|
121
|
+
suggestedAction: `Deadline "${deadline.label}" approaching. Verify status.`,
|
|
122
|
+
metadata: { deadline: deadline.label, expired: false, docType },
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
type: 'time_bomb', severity: 'info', nodeIds: [nodeId],
|
|
127
|
+
description: `Future deadline: "${deadline.label}".`,
|
|
128
|
+
suggestedAction: `Deadline "${deadline.label}" is upcoming. Track it.`,
|
|
129
|
+
metadata: { deadline: deadline.label, expired: false, docType },
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
/** Extract topic words for cross-doc completion matching. */
|
|
133
|
+
function topicWords(text) {
|
|
134
|
+
return new Set(text.toLowerCase().replace(/[^\w\s]/g, ' ').split(/\s+/).filter((w) => w.length >= 5));
|
|
135
|
+
}
|
|
136
|
+
/** Common words not useful for topic matching. */
|
|
137
|
+
const COMMON_WORDS = new Set([
|
|
138
|
+
'services', 'project', 'company', 'classic',
|
|
139
|
+
'document', 'created', 'space', 'pages', 'https',
|
|
140
|
+
'about', 'which', 'their', 'there', 'these', 'those',
|
|
141
|
+
'would', 'could', 'should', 'other', 'after', 'before',
|
|
142
|
+
]);
|
|
143
|
+
/** Find a node that may have completed the deadline referenced by another node. */
|
|
144
|
+
export function findCompletionNode(nodeId, nodeText, nodes) {
|
|
145
|
+
const topics = new Set([...topicWords(nodeText)].filter((w) => !COMMON_WORDS.has(w)));
|
|
146
|
+
if (topics.size < 2)
|
|
147
|
+
return undefined;
|
|
148
|
+
for (const n of nodes) {
|
|
149
|
+
if (n.id === nodeId)
|
|
150
|
+
continue;
|
|
151
|
+
const t = `${n.title} ${(n.content.facts ?? []).join(' ')} ${n.content.summary}`;
|
|
152
|
+
if (!COMPLETION_RE.test(t))
|
|
153
|
+
continue;
|
|
154
|
+
const otherTopics = new Set([...topicWords(t)].filter((w) => !COMMON_WORDS.has(w)));
|
|
155
|
+
const shared = [...otherTopics].filter((w) => topics.has(w));
|
|
156
|
+
if (shared.length >= 3)
|
|
157
|
+
return n.title;
|
|
158
|
+
}
|
|
159
|
+
return undefined;
|
|
160
|
+
}
|
|
161
|
+
//# sourceMappingURL=time-bomb-utils.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Time bomb detector.
|
|
3
|
+
* Finds date-dependent commitments that have expired or will expire soon.
|
|
4
|
+
* @module time-bombs
|
|
5
|
+
*/
|
|
6
|
+
import type { DetectorFn } from '@useody/platform-core';
|
|
7
|
+
import { extractDeadlines, classifyDeadline } from './time-bomb-utils.js';
|
|
8
|
+
/**
|
|
9
|
+
* Detect date-dependent commitments that may have expired.
|
|
10
|
+
* Parses dates without LLM; uses LLM for richer analysis when available.
|
|
11
|
+
*/
|
|
12
|
+
declare const detectTimeBombs: DetectorFn;
|
|
13
|
+
export { detectTimeBombs, extractDeadlines, classifyDeadline };
|
|
14
|
+
//# sourceMappingURL=time-bombs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"time-bombs.d.ts","sourceRoot":"","sources":["../src/time-bombs.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EAIV,UAAU,EAEX,MAAM,uBAAuB,CAAC;AAG/B,OAAO,EAEL,gBAAgB,EAChB,gBAAgB,EAEjB,MAAM,sBAAsB,CAAC;AAE9B;;;GAGG;AACH,QAAA,MAAM,eAAe,EAAE,UAgHtB,CAAC;AAQF,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,CAAC"}
|