@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.
Files changed (57) hide show
  1. package/LICENSE +198 -0
  2. package/README.md +42 -0
  3. package/dist/claim-comparison.d.ts +12 -0
  4. package/dist/claim-comparison.d.ts.map +1 -0
  5. package/dist/claim-comparison.js +108 -0
  6. package/dist/claim-nli.d.ts +15 -0
  7. package/dist/claim-nli.d.ts.map +1 -0
  8. package/dist/claim-nli.js +181 -0
  9. package/dist/consensus.d.ts +26 -0
  10. package/dist/consensus.d.ts.map +1 -0
  11. package/dist/consensus.js +211 -0
  12. package/dist/consultant-analysis.d.ts +20 -0
  13. package/dist/consultant-analysis.d.ts.map +1 -0
  14. package/dist/consultant-analysis.js +69 -0
  15. package/dist/consultant-prompts.d.ts +83 -0
  16. package/dist/consultant-prompts.d.ts.map +1 -0
  17. package/dist/consultant-prompts.js +135 -0
  18. package/dist/contradiction-helpers.d.ts +40 -0
  19. package/dist/contradiction-helpers.d.ts.map +1 -0
  20. package/dist/contradiction-helpers.js +163 -0
  21. package/dist/contradictions.d.ts +20 -0
  22. package/dist/contradictions.d.ts.map +1 -0
  23. package/dist/contradictions.js +235 -0
  24. package/dist/duplicates.d.ts +14 -0
  25. package/dist/duplicates.d.ts.map +1 -0
  26. package/dist/duplicates.js +95 -0
  27. package/dist/health-score.d.ts +13 -0
  28. package/dist/health-score.d.ts.map +1 -0
  29. package/dist/health-score.js +53 -0
  30. package/dist/helpers/index.d.ts +7 -0
  31. package/dist/helpers/index.d.ts.map +1 -0
  32. package/dist/helpers/index.js +7 -0
  33. package/dist/helpers/llm-timeout.d.ts +14 -0
  34. package/dist/helpers/llm-timeout.d.ts.map +1 -0
  35. package/dist/helpers/llm-timeout.js +30 -0
  36. package/dist/helpers/text-utils.d.ts +21 -0
  37. package/dist/helpers/text-utils.d.ts.map +1 -0
  38. package/dist/helpers/text-utils.js +63 -0
  39. package/dist/index.d.ts +17 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +14 -0
  42. package/dist/run-detection.d.ts +29 -0
  43. package/dist/run-detection.d.ts.map +1 -0
  44. package/dist/run-detection.js +46 -0
  45. package/dist/staleness.d.ts +21 -0
  46. package/dist/staleness.d.ts.map +1 -0
  47. package/dist/staleness.js +128 -0
  48. package/dist/time-bomb-utils.d.ts +23 -0
  49. package/dist/time-bomb-utils.d.ts.map +1 -0
  50. package/dist/time-bomb-utils.js +161 -0
  51. package/dist/time-bombs.d.ts +14 -0
  52. package/dist/time-bombs.d.ts.map +1 -0
  53. package/dist/time-bombs.js +113 -0
  54. package/dist/undocumented.d.ts +13 -0
  55. package/dist/undocumented.d.ts.map +1 -0
  56. package/dist/undocumented.js +84 -0
  57. package/package.json +48 -0
@@ -0,0 +1,113 @@
1
+ import { parseLlmJsonResponse } from '@useody/platform-core';
2
+ import { completeWithTimeout } from './helpers/llm-timeout.js';
3
+ import { DATE_PATTERN, extractDeadlines, classifyDeadline, findCompletionNode, } from './time-bomb-utils.js';
4
+ /**
5
+ * Detect date-dependent commitments that may have expired.
6
+ * Parses dates without LLM; uses LLM for richer analysis when available.
7
+ */
8
+ const detectTimeBombs = async (nodes, _edges, llm) => {
9
+ const detections = [];
10
+ const now = new Date();
11
+ const nowLabel = now.toISOString().split('T')[0];
12
+ for (const node of nodes) {
13
+ const facts = node.content.facts ?? [];
14
+ const raw = node.content.raw ?? '';
15
+ const text = `${node.title} ${facts.join(' ')} ${node.content.summary} ${raw}`;
16
+ if (!DATE_PATTERN.test(text))
17
+ continue;
18
+ if (!llm) {
19
+ const deadlines = extractDeadlines(text, now);
20
+ if (deadlines.length === 0)
21
+ continue;
22
+ const nodeDocType = node.metadata?.['docType'] ?? undefined;
23
+ for (const dl of deadlines) {
24
+ const det = classifyDeadline(dl, now, node.id, nowLabel, nodeDocType);
25
+ if (det)
26
+ detections.push(det);
27
+ }
28
+ continue;
29
+ }
30
+ const response = await completeWithTimeout(llm, [
31
+ {
32
+ role: 'system',
33
+ content: [
34
+ 'You analyze text for date-dependent commitments.',
35
+ `Today is ${nowLabel}.`,
36
+ 'Return JSON: {"hasTimeBomb":boolean,"deadline":string|null,',
37
+ '"expired":boolean,"description":string}',
38
+ 'Keep description under 30 words.',
39
+ ].join(' '),
40
+ },
41
+ { role: 'user', content: `Text: "${text.slice(0, 1000)}"` },
42
+ ], { temperature: 0, maxTokens: 150 });
43
+ if (!response)
44
+ continue;
45
+ const parsed = parseLlmJsonResponse(response);
46
+ if (parsed.data?.hasTimeBomb) {
47
+ detections.push({
48
+ type: 'time_bomb',
49
+ severity: parsed.data.expired ? 'critical' : 'warning',
50
+ nodeIds: [node.id],
51
+ description: parsed.data.description,
52
+ suggestedAction: parsed.data.expired
53
+ ? `Deadline "${parsed.data.deadline}" has passed. Update or remove.`
54
+ : `Deadline "${parsed.data.deadline}" approaching. Verify status.`,
55
+ metadata: {
56
+ deadline: parsed.data.deadline,
57
+ expired: parsed.data.expired,
58
+ },
59
+ });
60
+ }
61
+ }
62
+ // Deduplicate: group by deadline label, merge nodeIds
63
+ const byDeadline = new Map();
64
+ for (const det of detections) {
65
+ const key = det.metadata?.['deadline'] ?? det.description;
66
+ const existing = byDeadline.get(key);
67
+ if (existing) {
68
+ const merged = [...new Set([...existing.nodeIds, ...det.nodeIds])];
69
+ const sevOrder = { critical: 0, warning: 1, info: 2 };
70
+ const bestSev = sevOrder[det.severity] < sevOrder[existing.severity] ? det.severity : existing.severity;
71
+ byDeadline.set(key, {
72
+ ...existing,
73
+ severity: bestSev,
74
+ nodeIds: merged,
75
+ description: merged.length > 1
76
+ ? `${existing.description.replace(/\s*\(\d+ documents?\)$/g, '')} (${String(merged.length)} documents)`
77
+ : existing.description,
78
+ });
79
+ }
80
+ else {
81
+ byDeadline.set(key, det);
82
+ }
83
+ }
84
+ const deduped = [...byDeadline.values()];
85
+ const MAX_TIME_BOMBS = 20;
86
+ const capped = deduped.slice(0, MAX_TIME_BOMBS);
87
+ return capped.map((det) => {
88
+ if (det.metadata?.expired !== true)
89
+ return det;
90
+ const nodeId = det.nodeIds[0];
91
+ if (!nodeId)
92
+ return det;
93
+ const node = nodes.find((n) => n.id === nodeId);
94
+ if (!node)
95
+ return det;
96
+ const nodeText = `${node.title} ${(node.content.facts ?? []).join(' ')} ${node.content.summary}`;
97
+ const completionTitle = findCompletionNode(nodeId, nodeText, nodes);
98
+ if (!completionTitle)
99
+ return det;
100
+ return {
101
+ ...det,
102
+ severity: 'info',
103
+ description: `Deadline passed but may have been completed — see '${completionTitle}'`,
104
+ };
105
+ });
106
+ };
107
+ detectTimeBombs.preFilter = {
108
+ similarityThreshold: 0,
109
+ topK: 0,
110
+ requireAllNodes: true,
111
+ };
112
+ export { detectTimeBombs, extractDeadlines, classifyDeadline };
113
+ //# sourceMappingURL=time-bombs.js.map
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Undocumented topic detector.
3
+ * Finds topics frequently discussed in chat with no matching documentation.
4
+ * @module undocumented
5
+ */
6
+ import type { DetectorFn } from '@useody/platform-core';
7
+ /**
8
+ * Detect topics discussed frequently in chat with no documentation.
9
+ * Requires all nodes (requireAllNodes: true) to separate chat vs doc sources.
10
+ */
11
+ declare const detectUndocumented: DetectorFn;
12
+ export { detectUndocumented };
13
+ //# sourceMappingURL=undocumented.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"undocumented.d.ts","sourceRoot":"","sources":["../src/undocumented.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EAIV,UAAU,EAEX,MAAM,uBAAuB,CAAC;AAmE/B;;;GAGG;AACH,QAAA,MAAM,kBAAkB,EAAE,UA6CzB,CAAC;AAQF,OAAO,EAAE,kBAAkB,EAAE,CAAC"}
@@ -0,0 +1,84 @@
1
+ import { sharedTokens, lexicalScore, buildSignal, } from './helpers/text-utils.js';
2
+ const CHAT_SOURCE_TYPES = new Set(['slack', 'teams', 'discord', 'chat']);
3
+ const DOC_SOURCE_TYPES = new Set([
4
+ 'markdown', 'pdf', 'notion', 'confluence', 'linear', 'jira',
5
+ ]);
6
+ function cosineSimilarity(a, b) {
7
+ let dot = 0;
8
+ let normA = 0;
9
+ let normB = 0;
10
+ for (let i = 0; i < a.length; i++) {
11
+ dot += a[i] * b[i];
12
+ normA += a[i] * a[i];
13
+ normB += b[i] * b[i];
14
+ }
15
+ const denom = Math.sqrt(normA) * Math.sqrt(normB);
16
+ return denom === 0 ? 0 : dot / denom;
17
+ }
18
+ function buildClusters(chatNodes) {
19
+ const clusters = [];
20
+ const assigned = new Set();
21
+ for (const node of chatNodes) {
22
+ if (assigned.has(node.id))
23
+ continue;
24
+ if (node.embedding.length === 0)
25
+ continue;
26
+ const similar = chatNodes.filter((other) => other.id !== node.id &&
27
+ !assigned.has(other.id) &&
28
+ other.embedding.length > 0 &&
29
+ cosineSimilarity(node.embedding, other.embedding) > 0.64);
30
+ const clusterIds = [node.id, ...similar.map((s) => s.id)];
31
+ if (clusterIds.length < 3)
32
+ continue;
33
+ for (const id of clusterIds)
34
+ assigned.add(id);
35
+ const allFacts = [
36
+ ...(node.content.facts ?? []),
37
+ ...similar.flatMap((s) => s.content.facts ?? []),
38
+ ];
39
+ clusters.push({
40
+ topic: node.title,
41
+ nodeIds: clusterIds,
42
+ facts: [...new Set(allFacts)].slice(0, 10),
43
+ });
44
+ }
45
+ return clusters;
46
+ }
47
+ /**
48
+ * Detect topics discussed frequently in chat with no documentation.
49
+ * Requires all nodes (requireAllNodes: true) to separate chat vs doc sources.
50
+ */
51
+ const detectUndocumented = async (nodes, _edges, _llm) => {
52
+ const detections = [];
53
+ const chatNodes = nodes.filter((n) => CHAT_SOURCE_TYPES.has(n.content.source?.sourceType ?? ''));
54
+ const docNodes = nodes.filter((n) => DOC_SOURCE_TYPES.has(n.content.source?.sourceType ?? ''));
55
+ const clusters = buildClusters(chatNodes);
56
+ for (const cluster of clusters) {
57
+ const clusterSignal = buildSignal(cluster.topic, cluster.facts);
58
+ const hasDoc = docNodes.some((doc) => {
59
+ const docSignal = buildSignal(doc.title, doc.content.facts ?? []);
60
+ const shared = sharedTokens(clusterSignal.titleTokens, docSignal.titleTokens).length;
61
+ const lexical = lexicalScore(clusterSignal.tokens, docSignal.tokens);
62
+ return shared >= 2 || (shared >= 1 && lexical >= 0.28);
63
+ });
64
+ if (hasDoc)
65
+ continue;
66
+ const mentions = cluster.nodeIds.length;
67
+ detections.push({
68
+ type: 'undocumented',
69
+ severity: mentions >= 6 ? 'warning' : 'info',
70
+ nodeIds: cluster.nodeIds.slice(0, 10),
71
+ description: `Discussed ${mentions} times in chat with no documentation. ` +
72
+ `Key points: ${cluster.facts.slice(0, 5).join('; ')}`,
73
+ suggestedAction: `Create a document covering: ${cluster.facts.slice(0, 5).join('; ')}`,
74
+ });
75
+ }
76
+ return detections;
77
+ };
78
+ detectUndocumented.preFilter = {
79
+ similarityThreshold: 0,
80
+ topK: 0,
81
+ requireAllNodes: true,
82
+ };
83
+ export { detectUndocumented };
84
+ //# sourceMappingURL=undocumented.js.map
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@useody/detectors",
3
+ "description": "Five pure-function detectors for contradictions, duplicates, staleness, undocumented topics, and time bombs",
4
+ "version": "0.0.1",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "scripts": {
15
+ "build": "tsc",
16
+ "test": "vitest run",
17
+ "typecheck": "tsc --noEmit",
18
+ "lint": "eslint src/",
19
+ "clean": "rm -rf dist",
20
+ "prepublishOnly": "pnpm build"
21
+ },
22
+ "dependencies": {
23
+ "@useody/platform-core": "workspace:*"
24
+ },
25
+ "devDependencies": {
26
+ "vitest": "^3.0.0",
27
+ "typescript": "^5.7.0"
28
+ },
29
+ "engines": {
30
+ "node": ">=20.0.0"
31
+ },
32
+ "publishConfig": {
33
+ "access": "public"
34
+ },
35
+ "license": "Apache-2.0",
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "https://github.com/ufukkaraca/ody-platform.git",
39
+ "directory": "packages/detectors"
40
+ },
41
+ "files": [
42
+ "dist/**/*.js",
43
+ "dist/**/*.d.ts",
44
+ "dist/**/*.d.ts.map",
45
+ "README.md",
46
+ "LICENSE"
47
+ ]
48
+ }