timeline-truth 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -4
- package/docs/MCP-SETUP.md +3 -0
- package/package.json +2 -1
- package/src/cli.js +124 -0
- package/src/mcp-server.js +1 -1
- package/src/mcp-tools.js +2 -2
- package/src/timeline.js +151 -5
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Timeline Truth
|
|
2
2
|
|
|
3
|
-
Status: v0.
|
|
3
|
+
Status: v0.2.0 public release. MIT licensed. Requires Node.js 22 or newer.
|
|
4
4
|
|
|
5
5
|
Timeline Truth is a local MCP server for AI-agent TPM workflows: paste PRD/Jira/status notes,
|
|
6
6
|
CSV exports, launch checklists, or rough planning text; get timeline JSON,
|
|
@@ -30,8 +30,18 @@ single source. Then summarize the timeline, list gaps and assumptions, and show
|
|
|
30
30
|
the mermaid_gantt output. Do not infer missing dates or owners.
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
The server returns normalized items,
|
|
34
|
-
|
|
33
|
+
The server returns normalized items, confidence reasons, grouped follow-up
|
|
34
|
+
questions, gaps such as missing start/end dates, the default assumption that
|
|
35
|
+
dates were not inferred, and portable Mermaid output.
|
|
36
|
+
|
|
37
|
+
For a quick local smoke test without configuring an MCP client, run the CLI:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
timeline-truth examples/launch-checklist.md --format review
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The CLI reads from stdin when no file is provided, and can print `json`,
|
|
44
|
+
`markdown`, `mermaid_gantt`, `mermaid_timeline`, or `review` output.
|
|
35
45
|
|
|
36
46
|
For larger Markdown notes, `create_timeline` can parse only selected headings
|
|
37
47
|
and ignore the rest:
|
|
@@ -108,6 +118,7 @@ Optional global install:
|
|
|
108
118
|
|
|
109
119
|
```bash
|
|
110
120
|
npm install -g timeline-truth
|
|
121
|
+
timeline-truth examples/launch-checklist.md --format review
|
|
111
122
|
timeline-truth-mcp
|
|
112
123
|
```
|
|
113
124
|
|
|
@@ -125,7 +136,7 @@ For local development, use the checkout config in
|
|
|
125
136
|
- `validate_timeline`: report missing dates, owners, unknown dependencies,
|
|
126
137
|
circular dependencies, and impossible sequencing.
|
|
127
138
|
- `render_timeline`: render a normalized timeline as `mermaid_gantt`,
|
|
128
|
-
`mermaid_timeline`, or `
|
|
139
|
+
`mermaid_timeline`, `markdown`, or `review_report`.
|
|
129
140
|
- `refine_timeline`: apply edits while preserving evidence (`source_refs`) and
|
|
130
141
|
assumptions.
|
|
131
142
|
|
package/docs/MCP-SETUP.md
CHANGED
|
@@ -46,6 +46,7 @@ Optional global install:
|
|
|
46
46
|
|
|
47
47
|
```bash
|
|
48
48
|
npm install -g timeline-truth
|
|
49
|
+
timeline-truth examples/launch-checklist.md --format review
|
|
49
50
|
timeline-truth-mcp
|
|
50
51
|
```
|
|
51
52
|
|
|
@@ -81,5 +82,7 @@ Useful follow-up calls:
|
|
|
81
82
|
|
|
82
83
|
- `validate_timeline` after manual edits or agent refinements.
|
|
83
84
|
- `render_timeline` when you need only Mermaid or Markdown output.
|
|
85
|
+
- `render_timeline` with `review_report` when you need a paste-ready review
|
|
86
|
+
summary with confidence reasons and grouped follow-up questions.
|
|
84
87
|
- `refine_timeline` when a human answers a gap and you need to preserve
|
|
85
88
|
existing `source_refs`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "timeline-truth",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Open-source MCP server for compiling messy project planning inputs into evidence-preserving timelines.",
|
|
5
5
|
"author": "hilmimuktitama",
|
|
6
6
|
"license": "MIT",
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"homepage": "https://github.com/hilmimuktitama/timeline-truth#readme",
|
|
15
15
|
"type": "module",
|
|
16
16
|
"bin": {
|
|
17
|
+
"timeline-truth": "src/cli.js",
|
|
17
18
|
"timeline-truth-mcp": "src/mcp-server.js"
|
|
18
19
|
},
|
|
19
20
|
"files": [
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { basename, extname, resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
import { createTimeline } from "./timeline.js";
|
|
7
|
+
|
|
8
|
+
const FORMAT_ALIASES = {
|
|
9
|
+
json: "json",
|
|
10
|
+
markdown: "markdown",
|
|
11
|
+
md: "markdown",
|
|
12
|
+
mermaid: "mermaid_gantt",
|
|
13
|
+
mermaid_gantt: "mermaid_gantt",
|
|
14
|
+
gantt: "mermaid_gantt",
|
|
15
|
+
mermaid_timeline: "mermaid_timeline",
|
|
16
|
+
timeline: "mermaid_timeline",
|
|
17
|
+
review: "review_report",
|
|
18
|
+
review_report: "review_report"
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export function parseCliArgs(argv = []) {
|
|
22
|
+
const options = {
|
|
23
|
+
inputPath: "-",
|
|
24
|
+
sourceType: "text",
|
|
25
|
+
format: "json"
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
29
|
+
const arg = argv[index];
|
|
30
|
+
if (arg === "--type" || arg === "--source-type") {
|
|
31
|
+
options.sourceType = requireValue(argv, index, arg);
|
|
32
|
+
index += 1;
|
|
33
|
+
} else if (arg === "--format") {
|
|
34
|
+
options.format = requireValue(argv, index, arg);
|
|
35
|
+
index += 1;
|
|
36
|
+
} else if (arg === "--help" || arg === "-h") {
|
|
37
|
+
options.help = true;
|
|
38
|
+
} else if (arg.startsWith("--")) {
|
|
39
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
40
|
+
} else {
|
|
41
|
+
options.inputPath = arg;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (options.inputPath !== "-" && options.sourceType === "text") {
|
|
46
|
+
options.sourceType = inferSourceType(options.inputPath);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return options;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function runTimelineCli({ argv = process.argv.slice(2), stdin, cwd = process.cwd() } = {}) {
|
|
53
|
+
const options = parseCliArgs(argv);
|
|
54
|
+
if (options.help) return usage();
|
|
55
|
+
|
|
56
|
+
const content = options.inputPath === "-"
|
|
57
|
+
? stdin ?? readFileSync(0, "utf8")
|
|
58
|
+
: readFileSync(resolve(cwd, options.inputPath), "utf8");
|
|
59
|
+
const result = createTimeline({
|
|
60
|
+
sources: [
|
|
61
|
+
{
|
|
62
|
+
id: options.inputPath === "-" ? "stdin" : basename(options.inputPath),
|
|
63
|
+
type: options.sourceType,
|
|
64
|
+
path: options.inputPath === "-" ? undefined : options.inputPath,
|
|
65
|
+
content
|
|
66
|
+
}
|
|
67
|
+
]
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
return formatCliResult(result, options.format);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function formatCliResult(result, format = "json") {
|
|
74
|
+
const normalizedFormat = normalizeFormat(format);
|
|
75
|
+
if (normalizedFormat === "json") return JSON.stringify(result, null, 2);
|
|
76
|
+
if (normalizedFormat === "markdown") return result.renders.markdown.trimEnd();
|
|
77
|
+
if (normalizedFormat === "review_report") return result.renders.review_report.trimEnd();
|
|
78
|
+
if (normalizedFormat === "mermaid_timeline") return result.renders.mermaid_timeline.trimEnd();
|
|
79
|
+
return result.renders.mermaid_gantt.trimEnd();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function normalizeFormat(format) {
|
|
83
|
+
const normalized = String(format || "json").toLowerCase();
|
|
84
|
+
const mapped = FORMAT_ALIASES[normalized];
|
|
85
|
+
if (!mapped) {
|
|
86
|
+
throw new Error(`Unsupported format "${format}". Use json, markdown, mermaid_gantt, mermaid_timeline, or review.`);
|
|
87
|
+
}
|
|
88
|
+
return mapped;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function inferSourceType(inputPath) {
|
|
92
|
+
const extension = extname(inputPath).toLowerCase();
|
|
93
|
+
if (extension === ".md" || extension === ".markdown") return "markdown";
|
|
94
|
+
if (extension === ".csv") return "csv";
|
|
95
|
+
if (extension === ".json") return "json";
|
|
96
|
+
return "text";
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function requireValue(argv, index, option) {
|
|
100
|
+
const value = argv[index + 1];
|
|
101
|
+
if (!value || value.startsWith("--")) {
|
|
102
|
+
throw new Error(`Missing value for ${option}.`);
|
|
103
|
+
}
|
|
104
|
+
return value;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function usage() {
|
|
108
|
+
return [
|
|
109
|
+
"Usage: timeline-truth [file] [--type text|markdown|csv|json] [--format json|markdown|mermaid_gantt|mermaid_timeline|review]",
|
|
110
|
+
"",
|
|
111
|
+
"Reads stdin when no file is provided."
|
|
112
|
+
].join("\n");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const isDirectRun = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
116
|
+
|
|
117
|
+
if (isDirectRun) {
|
|
118
|
+
try {
|
|
119
|
+
process.stdout.write(`${runTimelineCli()}\n`);
|
|
120
|
+
} catch (error) {
|
|
121
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
122
|
+
process.exitCode = 1;
|
|
123
|
+
}
|
|
124
|
+
}
|
package/src/mcp-server.js
CHANGED
package/src/mcp-tools.js
CHANGED
|
@@ -79,7 +79,7 @@ export function listTimelineTools() {
|
|
|
79
79
|
},
|
|
80
80
|
{
|
|
81
81
|
name: "render_timeline",
|
|
82
|
-
description: "Render a normalized timeline as Mermaid Gantt, Mermaid timeline, or
|
|
82
|
+
description: "Render a normalized timeline as Mermaid Gantt, Mermaid timeline, compact Markdown, or a review report.",
|
|
83
83
|
inputSchema: {
|
|
84
84
|
type: "object",
|
|
85
85
|
required: ["timeline"],
|
|
@@ -88,7 +88,7 @@ export function listTimelineTools() {
|
|
|
88
88
|
timeline: TIMELINE_SCHEMA,
|
|
89
89
|
format: {
|
|
90
90
|
type: "string",
|
|
91
|
-
enum: ["mermaid_gantt", "mermaid_timeline", "markdown"],
|
|
91
|
+
enum: ["mermaid_gantt", "mermaid_timeline", "markdown", "review_report"],
|
|
92
92
|
default: "mermaid_gantt"
|
|
93
93
|
}
|
|
94
94
|
}
|
package/src/timeline.js
CHANGED
|
@@ -17,7 +17,7 @@ export function createTimeline(input = {}) {
|
|
|
17
17
|
gaps: [],
|
|
18
18
|
render: {
|
|
19
19
|
audience: "TPM/PM",
|
|
20
|
-
defaultFormats: ["mermaid_gantt", "mermaid_timeline", "markdown"]
|
|
20
|
+
defaultFormats: ["mermaid_gantt", "mermaid_timeline", "markdown", "review_report"]
|
|
21
21
|
}
|
|
22
22
|
});
|
|
23
23
|
const validation = validateTimeline(timeline);
|
|
@@ -26,17 +26,20 @@ export function createTimeline(input = {}) {
|
|
|
26
26
|
gaps: validation.gaps,
|
|
27
27
|
issues: validation.issues
|
|
28
28
|
};
|
|
29
|
+
const followups = buildFollowups(validatedTimeline);
|
|
29
30
|
|
|
30
31
|
return {
|
|
31
32
|
timeline: validatedTimeline,
|
|
32
33
|
assumptions: validatedTimeline.assumptions,
|
|
33
34
|
gaps: validatedTimeline.gaps,
|
|
34
35
|
issues: validation.issues,
|
|
36
|
+
followups,
|
|
35
37
|
noise_report: noiseReport,
|
|
36
38
|
renders: {
|
|
37
39
|
mermaid_gantt: renderTimeline(validatedTimeline, { format: "mermaid_gantt" }),
|
|
38
40
|
mermaid_timeline: renderTimeline(validatedTimeline, { format: "mermaid_timeline" }),
|
|
39
|
-
markdown: renderTimeline(validatedTimeline, { format: "markdown" })
|
|
41
|
+
markdown: renderTimeline(validatedTimeline, { format: "markdown" }),
|
|
42
|
+
review_report: renderTimeline(validatedTimeline, { format: "review_report" })
|
|
40
43
|
}
|
|
41
44
|
};
|
|
42
45
|
}
|
|
@@ -71,11 +74,13 @@ export function validateTimeline(timeline = {}) {
|
|
|
71
74
|
for (const item of normalized.items) {
|
|
72
75
|
for (const dependency of item.dependencies) {
|
|
73
76
|
if (!normalized.items.some((candidate) => candidate.title === dependency)) {
|
|
77
|
+
const suggestions = suggestDependencyTitles(normalized.items, dependency);
|
|
74
78
|
issues.push({
|
|
75
79
|
type: "unknown_dependency",
|
|
76
80
|
severity: "warning",
|
|
77
81
|
itemTitle: item.title,
|
|
78
82
|
dependency,
|
|
83
|
+
suggestions,
|
|
79
84
|
message: `Dependency "${dependency}" was not found in the timeline.`
|
|
80
85
|
});
|
|
81
86
|
}
|
|
@@ -122,6 +127,10 @@ export function renderTimeline(timeline = {}, options = {}) {
|
|
|
122
127
|
return renderMarkdown(normalized);
|
|
123
128
|
}
|
|
124
129
|
|
|
130
|
+
if (format === "review_report") {
|
|
131
|
+
return renderReviewReport(normalized);
|
|
132
|
+
}
|
|
133
|
+
|
|
125
134
|
return renderMermaidGantt(normalized);
|
|
126
135
|
}
|
|
127
136
|
|
|
@@ -175,7 +184,13 @@ function parseSource(source, index, importedAssumptions, input, noiseReport) {
|
|
|
175
184
|
}
|
|
176
185
|
|
|
177
186
|
function parseJsonSource(source, importedAssumptions) {
|
|
178
|
-
|
|
187
|
+
let parsed;
|
|
188
|
+
try {
|
|
189
|
+
parsed = typeof source.content === "string" ? JSON.parse(source.content) : source.content;
|
|
190
|
+
} catch (error) {
|
|
191
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
192
|
+
throw new Error(`Unable to parse JSON source "${source.id}": ${detail}`);
|
|
193
|
+
}
|
|
179
194
|
const rawItems = Array.isArray(parsed) ? parsed : parsed.items ?? [];
|
|
180
195
|
|
|
181
196
|
if (Array.isArray(parsed.assumptions)) {
|
|
@@ -311,7 +326,10 @@ function parseTextLine(line, sourceId, lineNumber) {
|
|
|
311
326
|
owner,
|
|
312
327
|
status,
|
|
313
328
|
dependencies,
|
|
314
|
-
confidence: dates.length > 0 ? 0.75 : 0.45
|
|
329
|
+
confidence: dates.length > 0 ? 0.75 : 0.45,
|
|
330
|
+
confidence_reason: dates.length > 0
|
|
331
|
+
? "Exact date evidence found in source text."
|
|
332
|
+
: "No exact dates found; timeline placement needs human follow-up."
|
|
315
333
|
};
|
|
316
334
|
|
|
317
335
|
return normalizeItem(item, [{ sourceId, line: lineNumber, text: trimmed }]);
|
|
@@ -406,7 +424,12 @@ function markdownRecordToItem(record, heading) {
|
|
|
406
424
|
time_window: fuzzyTarget,
|
|
407
425
|
date_text: fuzzyTarget,
|
|
408
426
|
exact_date_needed: Boolean(fuzzyTarget),
|
|
409
|
-
confidence: target ? 0.7 : 0.55
|
|
427
|
+
confidence: target ? 0.7 : 0.55,
|
|
428
|
+
confidence_reason: fuzzyTarget
|
|
429
|
+
? "Fuzzy date text was preserved for human review."
|
|
430
|
+
: target
|
|
431
|
+
? "Exact target date evidence found in Markdown table."
|
|
432
|
+
: "No target date found in Markdown table row."
|
|
410
433
|
};
|
|
411
434
|
}
|
|
412
435
|
|
|
@@ -421,6 +444,7 @@ function normalizeTimeline(timeline = {}) {
|
|
|
421
444
|
milestones,
|
|
422
445
|
assumptions: Array.isArray(timeline.assumptions) ? [...timeline.assumptions] : [],
|
|
423
446
|
gaps: Array.isArray(timeline.gaps) ? [...timeline.gaps] : [],
|
|
447
|
+
issues: Array.isArray(timeline.issues) ? [...timeline.issues] : [],
|
|
424
448
|
render: timeline.render && typeof timeline.render === "object" ? { ...timeline.render } : {}
|
|
425
449
|
};
|
|
426
450
|
}
|
|
@@ -446,6 +470,9 @@ function normalizeItem(item = {}, sourceRefs = []) {
|
|
|
446
470
|
status: blankToUndefined(item.status) || "unknown",
|
|
447
471
|
dependencies: dependencies.map((dependency) => String(dependency).trim()).filter(Boolean),
|
|
448
472
|
confidence: typeof item.confidence === "number" ? item.confidence : 0.6,
|
|
473
|
+
confidence_reason: typeof item.confidence_reason === "string"
|
|
474
|
+
? item.confidence_reason
|
|
475
|
+
: deriveConfidenceReason(item),
|
|
449
476
|
source_refs: normalizeSourceRefs(sourceRefs)
|
|
450
477
|
};
|
|
451
478
|
}
|
|
@@ -513,6 +540,24 @@ function dedupeCycles(cycles) {
|
|
|
513
540
|
});
|
|
514
541
|
}
|
|
515
542
|
|
|
543
|
+
function suggestDependencyTitles(items, dependency) {
|
|
544
|
+
const dependencyKey = normalizeDependencyKey(dependency);
|
|
545
|
+
if (!dependencyKey) return [];
|
|
546
|
+
|
|
547
|
+
return items
|
|
548
|
+
.filter((item) => {
|
|
549
|
+
const titleKey = normalizeDependencyKey(item.title);
|
|
550
|
+
const idKey = normalizeDependencyKey(item.id);
|
|
551
|
+
return titleKey === dependencyKey || idKey === dependencyKey;
|
|
552
|
+
})
|
|
553
|
+
.map((item) => item.title)
|
|
554
|
+
.slice(0, 3);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function normalizeDependencyKey(value) {
|
|
558
|
+
return String(value || "").toLowerCase().replace(/[^a-z0-9]+/g, "");
|
|
559
|
+
}
|
|
560
|
+
|
|
516
561
|
function renderMermaidGantt(timeline) {
|
|
517
562
|
const lines = ["gantt", " title Project Timeline", " dateFormat YYYY-MM-DD", " axisFormat %b %d", " section Plan"];
|
|
518
563
|
|
|
@@ -572,6 +617,107 @@ function renderMarkdown(timeline) {
|
|
|
572
617
|
return `${lines.join("\n")}\n`;
|
|
573
618
|
}
|
|
574
619
|
|
|
620
|
+
function renderReviewReport(timeline) {
|
|
621
|
+
const followups = buildFollowups(timeline);
|
|
622
|
+
const lines = ["## Timeline Review", "", "### Items"];
|
|
623
|
+
|
|
624
|
+
for (const item of timeline.items) {
|
|
625
|
+
const window = item.start ? `${item.start}${item.end ? ` to ${item.end}` : item.duration ? ` for ${item.duration}` : ""}` : item.time_window || "date needed";
|
|
626
|
+
lines.push(`- **${item.title}** (${item.type}, ${item.status}) - ${window}${item.owner ? ` - owner: ${item.owner}` : ""}`);
|
|
627
|
+
lines.push(` - Confidence: ${item.confidence} - ${item.confidence_reason}`);
|
|
628
|
+
if (item.source_refs.length > 0) {
|
|
629
|
+
lines.push(` - Source: ${formatSourceRef(item.source_refs[0])}`);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
if (timeline.gaps.length > 0) {
|
|
634
|
+
lines.push("", "### Follow-Up Questions");
|
|
635
|
+
for (const followup of followups.all) {
|
|
636
|
+
lines.push(`- ${followup.itemTitle}${followup.owner ? ` (${followup.owner})` : ""}: ${followup.question}`);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
if (timeline.issues.length > 0) {
|
|
641
|
+
lines.push("", "### Issues");
|
|
642
|
+
for (const issue of timeline.issues) {
|
|
643
|
+
const suggestions = issue.suggestions?.length ? ` Suggestions: ${issue.suggestions.join(", ")}.` : "";
|
|
644
|
+
lines.push(`- ${issue.severity}: ${issue.message}${suggestions}`);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
if (timeline.assumptions.length > 0) {
|
|
649
|
+
lines.push("", "### Assumptions");
|
|
650
|
+
for (const assumption of timeline.assumptions) {
|
|
651
|
+
lines.push(`- ${assumption}`);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
return `${lines.join("\n")}\n`;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function buildFollowups(timeline) {
|
|
659
|
+
const byTitle = new Map(timeline.items.map((item) => [item.title, item]));
|
|
660
|
+
const gapFollowups = timeline.gaps.map((gap) => {
|
|
661
|
+
const item = byTitle.get(gap.itemTitle);
|
|
662
|
+
return {
|
|
663
|
+
itemTitle: gap.itemTitle,
|
|
664
|
+
field: gap.field,
|
|
665
|
+
owner: item?.owner,
|
|
666
|
+
question: gap.question,
|
|
667
|
+
source_refs: gap.source_refs
|
|
668
|
+
};
|
|
669
|
+
});
|
|
670
|
+
const dependencyFollowups = timeline.issues
|
|
671
|
+
.filter((issue) => issue.type === "unknown_dependency")
|
|
672
|
+
.map((issue) => {
|
|
673
|
+
const item = byTitle.get(issue.itemTitle);
|
|
674
|
+
const suggestions = issue.suggestions?.length
|
|
675
|
+
? ` Did you mean ${issue.suggestions.join(", ")}?`
|
|
676
|
+
: "";
|
|
677
|
+
return {
|
|
678
|
+
itemTitle: issue.itemTitle,
|
|
679
|
+
field: "dependency",
|
|
680
|
+
dependency: issue.dependency,
|
|
681
|
+
owner: item?.owner,
|
|
682
|
+
question: `Confirm the dependency "${issue.dependency}" or add it to the timeline.${suggestions}`,
|
|
683
|
+
source_refs: item?.source_refs ?? []
|
|
684
|
+
};
|
|
685
|
+
});
|
|
686
|
+
const all = [...gapFollowups, ...dependencyFollowups];
|
|
687
|
+
const dateFollowups = all.filter((followup) => ["start", "end", "exact_date"].includes(followup.field));
|
|
688
|
+
|
|
689
|
+
return {
|
|
690
|
+
all,
|
|
691
|
+
by_field: groupBy(all, (followup) => followup.field),
|
|
692
|
+
by_owner: groupBy(all, (followup) => followup.owner || "Unassigned"),
|
|
693
|
+
by_date: groupBy(dateFollowups, (followup) => followup.field),
|
|
694
|
+
by_dependency: groupBy(dependencyFollowups, (followup) => followup.dependency)
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function groupBy(values, keyFn) {
|
|
699
|
+
return values.reduce((groups, value) => {
|
|
700
|
+
const key = keyFn(value);
|
|
701
|
+
groups[key] = groups[key] || [];
|
|
702
|
+
groups[key].push(value);
|
|
703
|
+
return groups;
|
|
704
|
+
}, {});
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function deriveConfidenceReason(item) {
|
|
708
|
+
if (item.time_window || item.date_text) return "Fuzzy date text was preserved for human review.";
|
|
709
|
+
if (item.start || item.end || item.duration) return "Structured date evidence was supplied.";
|
|
710
|
+
return "No date evidence was supplied.";
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
function formatSourceRef(sourceRef) {
|
|
714
|
+
const parts = [sourceRef.sourceId];
|
|
715
|
+
if (sourceRef.path) parts.push(sourceRef.path);
|
|
716
|
+
if (sourceRef.heading) parts.push(`heading "${sourceRef.heading}"`);
|
|
717
|
+
if (sourceRef.line) parts.push(`line ${sourceRef.line}`);
|
|
718
|
+
return parts.filter(Boolean).join(", ");
|
|
719
|
+
}
|
|
720
|
+
|
|
575
721
|
function parseCsv(content) {
|
|
576
722
|
const rows = [];
|
|
577
723
|
let row = [];
|