@ncukondo/search-hub 0.17.0 → 0.18.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/dist/cli/commands/query/assess.d.ts +15 -0
- package/dist/cli/commands/query/assess.d.ts.map +1 -0
- package/dist/cli/commands/query/assess.js +38 -0
- package/dist/cli/commands/query/assess.js.map +1 -0
- package/dist/cli/commands/query/iteration-log.d.ts +58 -0
- package/dist/cli/commands/query/iteration-log.d.ts.map +1 -0
- package/dist/cli/commands/query/iteration-log.js +115 -0
- package/dist/cli/commands/query/iteration-log.js.map +1 -0
- package/dist/cli/commands/query/log.d.ts +9 -0
- package/dist/cli/commands/query/log.d.ts.map +1 -0
- package/dist/cli/commands/query/log.js +64 -0
- package/dist/cli/commands/query/log.js.map +1 -0
- package/dist/cli/commands/review/init.d.ts.map +1 -1
- package/dist/cli/commands/review/init.js +5 -21
- package/dist/cli/commands/review/init.js.map +1 -1
- package/dist/cli/commands/review/schema.d.ts +199 -0
- package/dist/cli/commands/review/schema.d.ts.map +1 -0
- package/dist/cli/commands/review/schema.js +77 -0
- package/dist/cli/commands/review/schema.js.map +1 -0
- package/dist/cli/commands/review/types.d.ts +15 -70
- package/dist/cli/commands/review/types.d.ts.map +1 -1
- package/dist/cli/commands/review/types.js +2 -0
- package/dist/cli/commands/review/types.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +75 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/suggestions/rules.d.ts.map +1 -1
- package/dist/cli/suggestions/rules.js +22 -2
- package/dist/cli/suggestions/rules.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface AssessOptions {
|
|
2
|
+
verdict?: string | undefined;
|
|
3
|
+
precision?: string | undefined;
|
|
4
|
+
comment?: string | undefined;
|
|
5
|
+
}
|
|
6
|
+
export interface AssessResult {
|
|
7
|
+
success: boolean;
|
|
8
|
+
error?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Execute the query assess command.
|
|
12
|
+
* Validates inputs and appends an assessment entry to the log.
|
|
13
|
+
*/
|
|
14
|
+
export declare function executeQueryAssess(queryFile: string, options: AssessOptions): Promise<AssessResult>;
|
|
15
|
+
//# sourceMappingURL=assess.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"assess.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/query/assess.ts"],"names":[],"mappings":"AAcA,MAAM,WAAW,aAAa;IAC5B,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/B,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC9B;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;GAGG;AACH,wBAAsB,kBAAkB,CACtC,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,aAAa,GACrB,OAAO,CAAC,YAAY,CAAC,CAqCvB"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { access } from "node:fs/promises";
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import { formatTimestamp, appendLogEntry } from "./iteration-log.js";
|
|
4
|
+
async function executeQueryAssess(queryFile, options) {
|
|
5
|
+
if (!options.verdict && !options.precision && !options.comment) {
|
|
6
|
+
return {
|
|
7
|
+
success: false,
|
|
8
|
+
error: "At least one of --verdict, --precision, or --comment is required"
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
try {
|
|
12
|
+
await access(queryFile, constants.R_OK);
|
|
13
|
+
} catch {
|
|
14
|
+
return {
|
|
15
|
+
success: false,
|
|
16
|
+
error: `Query file not found: ${queryFile}`
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
const entry = {
|
|
20
|
+
date: formatTimestamp(),
|
|
21
|
+
type: "assessment"
|
|
22
|
+
};
|
|
23
|
+
if (options.verdict !== void 0) {
|
|
24
|
+
entry.verdict = options.verdict;
|
|
25
|
+
}
|
|
26
|
+
if (options.precision !== void 0) {
|
|
27
|
+
entry.precision = options.precision;
|
|
28
|
+
}
|
|
29
|
+
if (options.comment !== void 0) {
|
|
30
|
+
entry.comment = options.comment;
|
|
31
|
+
}
|
|
32
|
+
await appendLogEntry(queryFile, entry);
|
|
33
|
+
return { success: true };
|
|
34
|
+
}
|
|
35
|
+
export {
|
|
36
|
+
executeQueryAssess
|
|
37
|
+
};
|
|
38
|
+
//# sourceMappingURL=assess.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"assess.js","sources":["../../../../src/cli/commands/query/assess.ts"],"sourcesContent":["/**\n * Query Assess Command\n *\n * Records a structured assessment of the current query iteration\n * to the search iteration log.\n */\nimport { access } from 'node:fs/promises';\nimport { constants } from 'node:fs';\nimport {\n appendLogEntry,\n formatTimestamp,\n type AssessmentLogEntry,\n} from './iteration-log.js';\n\nexport interface AssessOptions {\n verdict?: string | undefined;\n precision?: string | undefined;\n comment?: string | undefined;\n}\n\nexport interface AssessResult {\n success: boolean;\n error?: string;\n}\n\n/**\n * Execute the query assess command.\n * Validates inputs and appends an assessment entry to the log.\n */\nexport async function executeQueryAssess(\n queryFile: string,\n options: AssessOptions,\n): Promise<AssessResult> {\n // Validate at least one option is provided\n if (!options.verdict && !options.precision && !options.comment) {\n return {\n success: false,\n error: 'At least one of --verdict, --precision, or --comment is required',\n };\n }\n\n // Validate query file exists\n try {\n await access(queryFile, constants.R_OK);\n } catch {\n return {\n success: false,\n error: `Query file not found: ${queryFile}`,\n };\n }\n\n const entry: AssessmentLogEntry = {\n date: formatTimestamp(),\n type: 'assessment',\n };\n\n if (options.verdict !== undefined) {\n entry.verdict = options.verdict;\n }\n if (options.precision !== undefined) {\n entry.precision = options.precision;\n }\n if (options.comment !== undefined) {\n entry.comment = options.comment;\n }\n\n await appendLogEntry(queryFile, entry);\n\n return { success: true };\n}\n"],"names":[],"mappings":";;;AA6BA,eAAsB,mBACpB,WACA,SACuB;AAEvB,MAAI,CAAC,QAAQ,WAAW,CAAC,QAAQ,aAAa,CAAC,QAAQ,SAAS;AAC9D,WAAO;AAAA,MACL,SAAS;AAAA,MACT,OAAO;AAAA,IAAA;AAAA,EAEX;AAGA,MAAI;AACF,UAAM,OAAO,WAAW,UAAU,IAAI;AAAA,EACxC,QAAQ;AACN,WAAO;AAAA,MACL,SAAS;AAAA,MACT,OAAO,yBAAyB,SAAS;AAAA,IAAA;AAAA,EAE7C;AAEA,QAAM,QAA4B;AAAA,IAChC,MAAM,gBAAA;AAAA,IACN,MAAM;AAAA,EAAA;AAGR,MAAI,QAAQ,YAAY,QAAW;AACjC,UAAM,UAAU,QAAQ;AAAA,EAC1B;AACA,MAAI,QAAQ,cAAc,QAAW;AACnC,UAAM,YAAY,QAAQ;AAAA,EAC5B;AACA,MAAI,QAAQ,YAAY,QAAW;AACjC,UAAM,UAAU,QAAQ;AAAA,EAC1B;AAEA,QAAM,eAAe,WAAW,KAAK;AAErC,SAAO,EAAE,SAAS,KAAA;AACpB;"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { CountResult, PreviewResult } from '../search.js';
|
|
2
|
+
export interface CountLogEntry {
|
|
3
|
+
date: string;
|
|
4
|
+
type: 'count';
|
|
5
|
+
query_hash: string;
|
|
6
|
+
counts: Record<string, number>;
|
|
7
|
+
total: number;
|
|
8
|
+
}
|
|
9
|
+
export interface PreviewLogEntry {
|
|
10
|
+
date: string;
|
|
11
|
+
type: 'preview';
|
|
12
|
+
query_hash: string;
|
|
13
|
+
counts: Record<string, number>;
|
|
14
|
+
total: number;
|
|
15
|
+
titles: Record<string, string[]>;
|
|
16
|
+
}
|
|
17
|
+
export interface AssessmentLogEntry {
|
|
18
|
+
date: string;
|
|
19
|
+
type: 'assessment';
|
|
20
|
+
verdict?: string;
|
|
21
|
+
precision?: string;
|
|
22
|
+
comment?: string;
|
|
23
|
+
}
|
|
24
|
+
export type LogEntry = CountLogEntry | PreviewLogEntry | AssessmentLogEntry;
|
|
25
|
+
/**
|
|
26
|
+
* Derive the log file path from a query file path.
|
|
27
|
+
* `{dir}/{basename}.yaml` → `{dir}/{basename}.search-log.yaml`
|
|
28
|
+
*/
|
|
29
|
+
export declare function getLogFilePath(queryFilePath: string): string;
|
|
30
|
+
/**
|
|
31
|
+
* Generate a timestamp in "YYYY-MM-DD HH:mm" format.
|
|
32
|
+
*/
|
|
33
|
+
export declare function formatTimestamp(date?: Date): string;
|
|
34
|
+
/**
|
|
35
|
+
* Compute a short hash of the query file content.
|
|
36
|
+
* Uses the same algorithm as session creation (SHA-256, first 8 hex chars).
|
|
37
|
+
*/
|
|
38
|
+
export declare function computeQueryHash(queryContent: string): string;
|
|
39
|
+
/**
|
|
40
|
+
* Build a CountLogEntry from count-only results.
|
|
41
|
+
*/
|
|
42
|
+
export declare function buildCountLogEntry(queryHash: string, results: CountResult[]): CountLogEntry;
|
|
43
|
+
/**
|
|
44
|
+
* Build a PreviewLogEntry from preview results.
|
|
45
|
+
* Stores at most `maxTitles` titles per provider to avoid log bloat.
|
|
46
|
+
*/
|
|
47
|
+
export declare function buildPreviewLogEntry(queryHash: string, results: PreviewResult[], maxTitles?: number): PreviewLogEntry;
|
|
48
|
+
/**
|
|
49
|
+
* Read log entries from the iteration log file for a given query file.
|
|
50
|
+
* Returns an empty array when the log file does not exist or is empty.
|
|
51
|
+
*/
|
|
52
|
+
export declare function readLogEntries(queryFilePath: string): Promise<LogEntry[]>;
|
|
53
|
+
/**
|
|
54
|
+
* Append a log entry to the iteration log file.
|
|
55
|
+
* Creates the file with a header comment if it does not exist.
|
|
56
|
+
*/
|
|
57
|
+
export declare function appendLogEntry(queryFilePath: string, entry: LogEntry): Promise<void>;
|
|
58
|
+
//# sourceMappingURL=iteration-log.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"iteration-log.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/query/iteration-log.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAI/D,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,OAAO,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,SAAS,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;CAClC;AAED,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,YAAY,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,MAAM,QAAQ,GAAG,aAAa,GAAG,eAAe,GAAG,kBAAkB,CAAC;AAI5E;;;GAGG;AACH,wBAAgB,cAAc,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,CAK5D;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,IAAI,GAAE,IAAiB,GAAG,MAAM,CAO/D;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAE7D;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,WAAW,EAAE,GACrB,aAAa,CAgBf;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,aAAa,EAAE,EACxB,SAAS,SAAI,GACZ,eAAe,CAqBjB;AAmBD;;;GAGG;AACH,wBAAsB,cAAc,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,CA2B/E;AAED;;;GAGG;AACH,wBAAsB,cAAc,CAClC,aAAa,EAAE,MAAM,EACrB,KAAK,EAAE,QAAQ,GACd,OAAO,CAAC,IAAI,CAAC,CAoBf"}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { writeFile, readFile } from "node:fs/promises";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { basename, dirname, join } from "node:path";
|
|
4
|
+
import { parse, stringify } from "yaml";
|
|
5
|
+
function getLogFilePath(queryFilePath) {
|
|
6
|
+
const dir = dirname(queryFilePath);
|
|
7
|
+
const base = basename(queryFilePath).replace(/\.ya?ml$/, "");
|
|
8
|
+
const logName = `${base}.search-log.yaml`;
|
|
9
|
+
return dir === "." ? logName : join(dir, logName);
|
|
10
|
+
}
|
|
11
|
+
function formatTimestamp(date = /* @__PURE__ */ new Date()) {
|
|
12
|
+
const year = date.getFullYear();
|
|
13
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
14
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
15
|
+
const hours = String(date.getHours()).padStart(2, "0");
|
|
16
|
+
const minutes = String(date.getMinutes()).padStart(2, "0");
|
|
17
|
+
return `${year}-${month}-${day} ${hours}:${minutes}`;
|
|
18
|
+
}
|
|
19
|
+
function computeQueryHash(queryContent) {
|
|
20
|
+
return createHash("sha256").update(queryContent).digest("hex").slice(0, 8);
|
|
21
|
+
}
|
|
22
|
+
function buildCountLogEntry(queryHash, results) {
|
|
23
|
+
const counts = {};
|
|
24
|
+
let total = 0;
|
|
25
|
+
for (const r of results) {
|
|
26
|
+
if (!r.error) {
|
|
27
|
+
counts[r.provider] = r.count;
|
|
28
|
+
total += r.count;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
date: formatTimestamp(),
|
|
33
|
+
type: "count",
|
|
34
|
+
query_hash: queryHash,
|
|
35
|
+
counts,
|
|
36
|
+
total
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function buildPreviewLogEntry(queryHash, results, maxTitles = 5) {
|
|
40
|
+
const counts = {};
|
|
41
|
+
const titles = {};
|
|
42
|
+
let total = 0;
|
|
43
|
+
for (const r of results) {
|
|
44
|
+
if (!r.error) {
|
|
45
|
+
counts[r.provider] = r.count;
|
|
46
|
+
total += r.count;
|
|
47
|
+
if (r.titles.length > 0) {
|
|
48
|
+
titles[r.provider] = r.titles.slice(0, maxTitles);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
date: formatTimestamp(),
|
|
54
|
+
type: "preview",
|
|
55
|
+
query_hash: queryHash,
|
|
56
|
+
counts,
|
|
57
|
+
total,
|
|
58
|
+
titles
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
async function readRawLog(logPath) {
|
|
62
|
+
try {
|
|
63
|
+
return await readFile(logPath, "utf-8");
|
|
64
|
+
} catch (error) {
|
|
65
|
+
if (error.code === "ENOENT") {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
async function readLogEntries(queryFilePath) {
|
|
72
|
+
const logPath = getLogFilePath(queryFilePath);
|
|
73
|
+
const content = await readRawLog(logPath);
|
|
74
|
+
if (content === null || !content.trim()) {
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
const parsed = parse(content);
|
|
78
|
+
if (parsed === null || parsed === void 0) {
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
if (!Array.isArray(parsed)) {
|
|
82
|
+
return [];
|
|
83
|
+
}
|
|
84
|
+
return parsed.filter(
|
|
85
|
+
(entry) => entry !== null && typeof entry === "object" && typeof entry.type === "string" && ["count", "preview", "assessment"].includes(entry.type)
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
async function appendLogEntry(queryFilePath, entry) {
|
|
89
|
+
const logPath = getLogFilePath(queryFilePath);
|
|
90
|
+
const queryBasename = basename(queryFilePath);
|
|
91
|
+
const existing = await readRawLog(logPath);
|
|
92
|
+
const entryYaml = stringify([entry], { lineWidth: 0 }).trimEnd();
|
|
93
|
+
let content;
|
|
94
|
+
if (existing === null) {
|
|
95
|
+
const header = `# Search iteration log for ${queryBasename}
|
|
96
|
+
# Auto-generated by search-hub. You can also edit this file manually.
|
|
97
|
+
|
|
98
|
+
`;
|
|
99
|
+
content = header + entryYaml + "\n";
|
|
100
|
+
} else {
|
|
101
|
+
const trimmed = existing.trimEnd();
|
|
102
|
+
content = trimmed + "\n\n" + entryYaml + "\n";
|
|
103
|
+
}
|
|
104
|
+
await writeFile(logPath, content, "utf-8");
|
|
105
|
+
}
|
|
106
|
+
export {
|
|
107
|
+
appendLogEntry,
|
|
108
|
+
buildCountLogEntry,
|
|
109
|
+
buildPreviewLogEntry,
|
|
110
|
+
computeQueryHash,
|
|
111
|
+
formatTimestamp,
|
|
112
|
+
getLogFilePath,
|
|
113
|
+
readLogEntries
|
|
114
|
+
};
|
|
115
|
+
//# sourceMappingURL=iteration-log.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"iteration-log.js","sources":["../../../../src/cli/commands/query/iteration-log.ts"],"sourcesContent":["/**\n * Query iteration log I/O.\n *\n * Reads and appends entries to a search iteration log file\n * that lives alongside the query YAML file.\n */\nimport { readFile, writeFile } from 'node:fs/promises';\nimport { createHash } from 'node:crypto';\nimport { basename, dirname, join } from 'node:path';\nimport { parse, stringify } from 'yaml';\nimport type { CountResult, PreviewResult } from '../search.js';\n\n// ── Types ───────────────────────────────────────────────────────────\n\nexport interface CountLogEntry {\n date: string;\n type: 'count';\n query_hash: string;\n counts: Record<string, number>;\n total: number;\n}\n\nexport interface PreviewLogEntry {\n date: string;\n type: 'preview';\n query_hash: string;\n counts: Record<string, number>;\n total: number;\n titles: Record<string, string[]>;\n}\n\nexport interface AssessmentLogEntry {\n date: string;\n type: 'assessment';\n verdict?: string;\n precision?: string;\n comment?: string;\n}\n\nexport type LogEntry = CountLogEntry | PreviewLogEntry | AssessmentLogEntry;\n\n// ── Helpers ─────────────────────────────────────────────────────────\n\n/**\n * Derive the log file path from a query file path.\n * `{dir}/{basename}.yaml` → `{dir}/{basename}.search-log.yaml`\n */\nexport function getLogFilePath(queryFilePath: string): string {\n const dir = dirname(queryFilePath);\n const base = basename(queryFilePath).replace(/\\.ya?ml$/, '');\n const logName = `${base}.search-log.yaml`;\n return dir === '.' ? logName : join(dir, logName);\n}\n\n/**\n * Generate a timestamp in \"YYYY-MM-DD HH:mm\" format.\n */\nexport function formatTimestamp(date: Date = new Date()): string {\n const year = date.getFullYear();\n const month = String(date.getMonth() + 1).padStart(2, '0');\n const day = String(date.getDate()).padStart(2, '0');\n const hours = String(date.getHours()).padStart(2, '0');\n const minutes = String(date.getMinutes()).padStart(2, '0');\n return `${year}-${month}-${day} ${hours}:${minutes}`;\n}\n\n/**\n * Compute a short hash of the query file content.\n * Uses the same algorithm as session creation (SHA-256, first 8 hex chars).\n */\nexport function computeQueryHash(queryContent: string): string {\n return createHash('sha256').update(queryContent).digest('hex').slice(0, 8);\n}\n\n/**\n * Build a CountLogEntry from count-only results.\n */\nexport function buildCountLogEntry(\n queryHash: string,\n results: CountResult[],\n): CountLogEntry {\n const counts: Record<string, number> = {};\n let total = 0;\n for (const r of results) {\n if (!r.error) {\n counts[r.provider] = r.count;\n total += r.count;\n }\n }\n return {\n date: formatTimestamp(),\n type: 'count',\n query_hash: queryHash,\n counts,\n total,\n };\n}\n\n/**\n * Build a PreviewLogEntry from preview results.\n * Stores at most `maxTitles` titles per provider to avoid log bloat.\n */\nexport function buildPreviewLogEntry(\n queryHash: string,\n results: PreviewResult[],\n maxTitles = 5,\n): PreviewLogEntry {\n const counts: Record<string, number> = {};\n const titles: Record<string, string[]> = {};\n let total = 0;\n for (const r of results) {\n if (!r.error) {\n counts[r.provider] = r.count;\n total += r.count;\n if (r.titles.length > 0) {\n titles[r.provider] = r.titles.slice(0, maxTitles);\n }\n }\n }\n return {\n date: formatTimestamp(),\n type: 'preview',\n query_hash: queryHash,\n counts,\n total,\n titles,\n };\n}\n\n// ── Read / Write ────────────────────────────────────────────────────\n\n/**\n * Read raw content of the log file, preserving comments.\n * Returns null when the file does not exist.\n */\nasync function readRawLog(logPath: string): Promise<string | null> {\n try {\n return await readFile(logPath, 'utf-8');\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') {\n return null;\n }\n throw error;\n }\n}\n\n/**\n * Read log entries from the iteration log file for a given query file.\n * Returns an empty array when the log file does not exist or is empty.\n */\nexport async function readLogEntries(queryFilePath: string): Promise<LogEntry[]> {\n const logPath = getLogFilePath(queryFilePath);\n const content = await readRawLog(logPath);\n\n if (content === null || !content.trim()) {\n return [];\n }\n\n const parsed = parse(content);\n\n // YAML with only comments parses to null\n if (parsed === null || parsed === undefined) {\n return [];\n }\n\n if (!Array.isArray(parsed)) {\n return [];\n }\n\n // Filter out malformed entries that lack a valid type field\n return parsed.filter(\n (entry): entry is LogEntry =>\n entry !== null &&\n typeof entry === 'object' &&\n typeof entry.type === 'string' &&\n ['count', 'preview', 'assessment'].includes(entry.type),\n );\n}\n\n/**\n * Append a log entry to the iteration log file.\n * Creates the file with a header comment if it does not exist.\n */\nexport async function appendLogEntry(\n queryFilePath: string,\n entry: LogEntry,\n): Promise<void> {\n const logPath = getLogFilePath(queryFilePath);\n const queryBasename = basename(queryFilePath);\n const existing = await readRawLog(logPath);\n\n // Stringify the single entry as a YAML sequence item\n const entryYaml = stringify([entry], { lineWidth: 0 }).trimEnd();\n\n let content: string;\n if (existing === null) {\n const header =\n `# Search iteration log for ${queryBasename}\\n` +\n `# Auto-generated by search-hub. You can also edit this file manually.\\n\\n`;\n content = header + entryYaml + '\\n';\n } else {\n const trimmed = existing.trimEnd();\n content = trimmed + '\\n\\n' + entryYaml + '\\n';\n }\n\n await writeFile(logPath, content, 'utf-8');\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { LogEntry } from './iteration-log.js';
|
|
2
|
+
export interface LogOutputOptions {
|
|
3
|
+
json?: boolean | undefined;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Format log entries for display.
|
|
7
|
+
*/
|
|
8
|
+
export declare function formatLogOutput(entries: LogEntry[], options?: LogOutputOptions): string;
|
|
9
|
+
//# sourceMappingURL=log.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"log.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/query/log.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EACV,QAAQ,EAIT,MAAM,oBAAoB,CAAC;AAE5B,MAAM,WAAW,gBAAgB;IAC/B,IAAI,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CAC5B;AAED;;GAEG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,QAAQ,EAAE,EACnB,OAAO,CAAC,EAAE,gBAAgB,GACzB,MAAM,CA2BR"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
function formatLogOutput(entries, options) {
|
|
2
|
+
if (options?.json) {
|
|
3
|
+
return JSON.stringify(entries, null, 2);
|
|
4
|
+
}
|
|
5
|
+
if (entries.length === 0) {
|
|
6
|
+
return "No iteration log entries.";
|
|
7
|
+
}
|
|
8
|
+
const lines = [];
|
|
9
|
+
for (const entry of entries) {
|
|
10
|
+
switch (entry.type) {
|
|
11
|
+
case "count":
|
|
12
|
+
lines.push(formatCountEntry(entry));
|
|
13
|
+
break;
|
|
14
|
+
case "preview":
|
|
15
|
+
lines.push(formatPreviewEntry(entry));
|
|
16
|
+
break;
|
|
17
|
+
case "assessment":
|
|
18
|
+
lines.push(formatAssessmentEntry(entry));
|
|
19
|
+
break;
|
|
20
|
+
}
|
|
21
|
+
lines.push("");
|
|
22
|
+
}
|
|
23
|
+
return lines.join("\n").trimEnd();
|
|
24
|
+
}
|
|
25
|
+
function formatCountEntry(entry) {
|
|
26
|
+
const parts = [];
|
|
27
|
+
parts.push(`[${entry.date}] count (${entry.query_hash})`);
|
|
28
|
+
for (const [provider, count] of Object.entries(entry.counts)) {
|
|
29
|
+
parts.push(` ${provider}: ${count}`);
|
|
30
|
+
}
|
|
31
|
+
parts.push(` total: ${entry.total}`);
|
|
32
|
+
return parts.join("\n");
|
|
33
|
+
}
|
|
34
|
+
function formatPreviewEntry(entry) {
|
|
35
|
+
const parts = [];
|
|
36
|
+
parts.push(`[${entry.date}] preview (${entry.query_hash})`);
|
|
37
|
+
for (const [provider, count] of Object.entries(entry.counts)) {
|
|
38
|
+
parts.push(` ${provider}: ${count}`);
|
|
39
|
+
const providerTitles = entry.titles[provider];
|
|
40
|
+
if (providerTitles) {
|
|
41
|
+
for (const title of providerTitles) {
|
|
42
|
+
parts.push(` • ${title}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
parts.push(` total: ${entry.total}`);
|
|
47
|
+
return parts.join("\n");
|
|
48
|
+
}
|
|
49
|
+
function formatAssessmentEntry(entry) {
|
|
50
|
+
const parts = [];
|
|
51
|
+
const meta = [];
|
|
52
|
+
if (entry.verdict) meta.push(`verdict: ${entry.verdict}`);
|
|
53
|
+
if (entry.precision) meta.push(`precision: ${entry.precision}`);
|
|
54
|
+
const metaStr = meta.length > 0 ? ` ${meta.join(", ")}` : "";
|
|
55
|
+
parts.push(`[${entry.date}] assessment:${metaStr}`);
|
|
56
|
+
if (entry.comment) {
|
|
57
|
+
parts.push(` ${entry.comment}`);
|
|
58
|
+
}
|
|
59
|
+
return parts.join("\n");
|
|
60
|
+
}
|
|
61
|
+
export {
|
|
62
|
+
formatLogOutput
|
|
63
|
+
};
|
|
64
|
+
//# sourceMappingURL=log.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"log.js","sources":["../../../../src/cli/commands/query/log.ts"],"sourcesContent":["/**\n * Query Log Command\n *\n * Formats and displays the query iteration log.\n */\nimport type {\n LogEntry,\n CountLogEntry,\n PreviewLogEntry,\n AssessmentLogEntry,\n} from './iteration-log.js';\n\nexport interface LogOutputOptions {\n json?: boolean | undefined;\n}\n\n/**\n * Format log entries for display.\n */\nexport function formatLogOutput(\n entries: LogEntry[],\n options?: LogOutputOptions,\n): string {\n if (options?.json) {\n return JSON.stringify(entries, null, 2);\n }\n\n if (entries.length === 0) {\n return 'No iteration log entries.';\n }\n\n const lines: string[] = [];\n\n for (const entry of entries) {\n switch (entry.type) {\n case 'count':\n lines.push(formatCountEntry(entry));\n break;\n case 'preview':\n lines.push(formatPreviewEntry(entry));\n break;\n case 'assessment':\n lines.push(formatAssessmentEntry(entry));\n break;\n }\n lines.push('');\n }\n\n return lines.join('\\n').trimEnd();\n}\n\nfunction formatCountEntry(entry: CountLogEntry): string {\n const parts: string[] = [];\n parts.push(`[${entry.date}] count (${entry.query_hash})`);\n for (const [provider, count] of Object.entries(entry.counts)) {\n parts.push(` ${provider}: ${count}`);\n }\n parts.push(` total: ${entry.total}`);\n return parts.join('\\n');\n}\n\nfunction formatPreviewEntry(entry: PreviewLogEntry): string {\n const parts: string[] = [];\n parts.push(`[${entry.date}] preview (${entry.query_hash})`);\n for (const [provider, count] of Object.entries(entry.counts)) {\n parts.push(` ${provider}: ${count}`);\n const providerTitles = entry.titles[provider];\n if (providerTitles) {\n for (const title of providerTitles) {\n parts.push(` • ${title}`);\n }\n }\n }\n parts.push(` total: ${entry.total}`);\n return parts.join('\\n');\n}\n\nfunction formatAssessmentEntry(entry: AssessmentLogEntry): string {\n const parts: string[] = [];\n const meta: string[] = [];\n if (entry.verdict) meta.push(`verdict: ${entry.verdict}`);\n if (entry.precision) meta.push(`precision: ${entry.precision}`);\n const metaStr = meta.length > 0 ? ` ${meta.join(', ')}` : '';\n parts.push(`[${entry.date}] assessment:${metaStr}`);\n if (entry.comment) {\n parts.push(` ${entry.comment}`);\n }\n return parts.join('\\n');\n}\n"],"names":[],"mappings":"AAmBO,SAAS,gBACd,SACA,SACQ;AACR,MAAI,SAAS,MAAM;AACjB,WAAO,KAAK,UAAU,SAAS,MAAM,CAAC;AAAA,EACxC;AAEA,MAAI,QAAQ,WAAW,GAAG;AACxB,WAAO;AAAA,EACT;AAEA,QAAM,QAAkB,CAAA;AAExB,aAAW,SAAS,SAAS;AAC3B,YAAQ,MAAM,MAAA;AAAA,MACZ,KAAK;AACH,cAAM,KAAK,iBAAiB,KAAK,CAAC;AAClC;AAAA,MACF,KAAK;AACH,cAAM,KAAK,mBAAmB,KAAK,CAAC;AACpC;AAAA,MACF,KAAK;AACH,cAAM,KAAK,sBAAsB,KAAK,CAAC;AACvC;AAAA,IAAA;AAEJ,UAAM,KAAK,EAAE;AAAA,EACf;AAEA,SAAO,MAAM,KAAK,IAAI,EAAE,QAAA;AAC1B;AAEA,SAAS,iBAAiB,OAA8B;AACtD,QAAM,QAAkB,CAAA;AACxB,QAAM,KAAK,IAAI,MAAM,IAAI,YAAY,MAAM,UAAU,GAAG;AACxD,aAAW,CAAC,UAAU,KAAK,KAAK,OAAO,QAAQ,MAAM,MAAM,GAAG;AAC5D,UAAM,KAAK,KAAK,QAAQ,KAAK,KAAK,EAAE;AAAA,EACtC;AACA,QAAM,KAAK,YAAY,MAAM,KAAK,EAAE;AACpC,SAAO,MAAM,KAAK,IAAI;AACxB;AAEA,SAAS,mBAAmB,OAAgC;AAC1D,QAAM,QAAkB,CAAA;AACxB,QAAM,KAAK,IAAI,MAAM,IAAI,cAAc,MAAM,UAAU,GAAG;AAC1D,aAAW,CAAC,UAAU,KAAK,KAAK,OAAO,QAAQ,MAAM,MAAM,GAAG;AAC5D,UAAM,KAAK,KAAK,QAAQ,KAAK,KAAK,EAAE;AACpC,UAAM,iBAAiB,MAAM,OAAO,QAAQ;AAC5C,QAAI,gBAAgB;AAClB,iBAAW,SAAS,gBAAgB;AAClC,cAAM,KAAK,SAAS,KAAK,EAAE;AAAA,MAC7B;AAAA,IACF;AAAA,EACF;AACA,QAAM,KAAK,YAAY,MAAM,KAAK,EAAE;AACpC,SAAO,MAAM,KAAK,IAAI;AACxB;AAEA,SAAS,sBAAsB,OAAmC;AAChE,QAAM,QAAkB,CAAA;AACxB,QAAM,OAAiB,CAAA;AACvB,MAAI,MAAM,QAAS,MAAK,KAAK,YAAY,MAAM,OAAO,EAAE;AACxD,MAAI,MAAM,UAAW,MAAK,KAAK,cAAc,MAAM,SAAS,EAAE;AAC9D,QAAM,UAAU,KAAK,SAAS,IAAI,IAAI,KAAK,KAAK,IAAI,CAAC,KAAK;AAC1D,QAAM,KAAK,IAAI,MAAM,IAAI,gBAAgB,OAAO,EAAE;AAClD,MAAI,MAAM,SAAS;AACjB,UAAM,KAAK,KAAK,MAAM,OAAO,EAAE;AAAA,EACjC;AACA,SAAO,MAAM,KAAK,IAAI;AACxB;"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/review/init.ts"],"names":[],"mappings":"AAAA;;GAEG;
|
|
1
|
+
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/review/init.ts"],"names":[],"mappings":"AAAA;;GAEG;AAYH,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,gBAAgB;IAC/B,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAyDD;;GAEG;AACH,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,iBAAiB,EAC1B,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,gBAAgB,CAAC,CA6E3B"}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { join
|
|
2
|
-
import { access, mkdir, writeFile
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { access, mkdir, writeFile } from "node:fs/promises";
|
|
3
3
|
import { stringify } from "yaml";
|
|
4
4
|
import { loadSession } from "../../../session/manager.js";
|
|
5
5
|
import { loadResults } from "../../../session/results-io.js";
|
|
6
6
|
import { deduplicateForReview } from "./dedup.js";
|
|
7
|
+
import { generateReviewJSONSchema } from "./schema.js";
|
|
7
8
|
function formatAuthors(authors) {
|
|
8
9
|
return authors.map((a) => {
|
|
9
10
|
const parts = [];
|
|
@@ -38,20 +39,6 @@ function articleToEntry(article) {
|
|
|
38
39
|
}
|
|
39
40
|
return entry;
|
|
40
41
|
}
|
|
41
|
-
async function findSchemaSource() {
|
|
42
|
-
const possiblePaths = [
|
|
43
|
-
join(dirname(import.meta.url.replace("file://", "")), "../../../../schemas/review.schema.json"),
|
|
44
|
-
join(process.cwd(), "schemas/review.schema.json")
|
|
45
|
-
];
|
|
46
|
-
for (const path of possiblePaths) {
|
|
47
|
-
try {
|
|
48
|
-
await access(path);
|
|
49
|
-
return path;
|
|
50
|
-
} catch {
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
throw new Error("Could not find review.schema.json");
|
|
54
|
-
}
|
|
55
42
|
async function executeReviewInit(options, sessionsDir) {
|
|
56
43
|
const sessionDir = join(sessionsDir, options.sessionId);
|
|
57
44
|
const session = await loadSession(options.sessionId, sessionsDir);
|
|
@@ -98,11 +85,8 @@ async function executeReviewInit(options, sessionsDir) {
|
|
|
98
85
|
);
|
|
99
86
|
await writeFile(reviewsPath, finalContent, "utf-8");
|
|
100
87
|
const schemaDestPath = join(internalDir, "review.schema.json");
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
await copyFile(schemaSourcePath, schemaDestPath);
|
|
104
|
-
} catch {
|
|
105
|
-
}
|
|
88
|
+
const jsonSchema = generateReviewJSONSchema();
|
|
89
|
+
await writeFile(schemaDestPath, JSON.stringify(jsonSchema, null, 2) + "\n", "utf-8");
|
|
106
90
|
return {
|
|
107
91
|
reviewsPath,
|
|
108
92
|
articleCount: articleEntries.length,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"init.js","sources":["../../../../src/cli/commands/review/init.ts"],"sourcesContent":["/**\n * review init command - Generate reviews.yaml from session results\n */\n\nimport { join
|
|
1
|
+
{"version":3,"file":"init.js","sources":["../../../../src/cli/commands/review/init.ts"],"sourcesContent":["/**\n * review init command - Generate reviews.yaml from session results\n */\n\nimport { join } from 'node:path';\nimport { writeFile, mkdir, access } from 'node:fs/promises';\nimport { stringify as stringifyYaml } from 'yaml';\nimport type { Article, Author, ProviderName } from '../../../providers/base/types.js';\nimport { loadSession } from '../../../session/manager.js';\nimport { loadResults } from '../../../session/results-io.js';\nimport { deduplicateForReview } from './dedup.js';\nimport { generateReviewJSONSchema } from './schema.js';\nimport type { ArticleEntry, ReviewFile, MergedSource } from './types.js';\n\nexport interface ReviewInitOptions {\n sessionId: string;\n force?: boolean;\n}\n\nexport interface ReviewInitResult {\n reviewsPath: string;\n articleCount: number;\n duplicatesRemoved: number;\n}\n\n/**\n * Format authors array to string\n */\nfunction formatAuthors(authors: Author[]): string {\n return authors\n .map((a) => {\n const parts: string[] = [];\n if (a.family) parts.push(a.family);\n if (a.given) parts.push(a.given.charAt(0));\n return parts.join(' ');\n })\n .join(', ');\n}\n\n/**\n * Extract year from publication date\n */\nfunction extractYear(publicationDate?: string): string | undefined {\n if (!publicationDate) return undefined;\n const year = publicationDate.substring(0, 4);\n return /^\\d{4}$/.test(year) ? year : undefined;\n}\n\n/**\n * Convert Article to ArticleEntry for review file\n */\nfunction articleToEntry(article: Article & { mergedFrom?: MergedSource[] }): ArticleEntry {\n const entry: ArticleEntry = {\n title: article.title,\n reviews: [],\n };\n\n // Add identifiers\n if (article.doi) entry.doi = article.doi;\n if (article.pmid) entry.pmid = article.pmid;\n if (article.scopusId) entry.scopusId = article.scopusId;\n if (article.arxivId) entry.arxivId = article.arxivId;\n if (article.ericId) entry.ericId = article.ericId;\n\n // Add bibliographic info\n if (article.authors && article.authors.length > 0) {\n entry.authors = formatAuthors(article.authors);\n }\n const year = extractYear(article.publicationDate);\n if (year) entry.year = year;\n if (article.abstract) entry.abstract = article.abstract;\n\n // Add deduplication tracking\n if (article.mergedFrom && article.mergedFrom.length > 0) {\n entry.mergedFrom = article.mergedFrom;\n }\n\n return entry;\n}\n\n/**\n * Execute review init command\n */\nexport async function executeReviewInit(\n options: ReviewInitOptions,\n sessionsDir: string\n): Promise<ReviewInitResult> {\n const sessionDir = join(sessionsDir, options.sessionId);\n\n // Load session file\n const session = await loadSession(options.sessionId, sessionsDir);\n\n // Check if .internal/reviews.yaml already exists\n const internalDir = join(sessionDir, '.internal');\n const reviewsPath = join(internalDir, 'reviews.yaml');\n try {\n await access(reviewsPath);\n if (!options.force) {\n throw new Error(`reviews.yaml already exists. Use --force to overwrite.`);\n }\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {\n throw err;\n }\n }\n\n // Create .internal/ directory\n await mkdir(internalDir, { recursive: true });\n\n // Load all results from session\n const allArticles: Article[] = [];\n const providers = Object.keys(session.databases) as ProviderName[];\n\n for (const provider of providers) {\n const dbStatus = session.databases[provider];\n if (!dbStatus) continue;\n const articles = await loadResults(sessionDir, provider);\n allArticles.push(...articles);\n }\n\n // Deduplicate with mergedFrom tracking\n const { articles: dedupedArticles, duplicatesRemoved } = deduplicateForReview(allArticles);\n\n // Convert to ArticleEntry format\n const articleEntries = dedupedArticles.map(articleToEntry);\n\n // Build review file\n const reviewFile: ReviewFile = {\n sessionId: options.sessionId,\n articles: articleEntries,\n };\n\n // Generate YAML with schema reference comment\n const yamlContent = stringifyYaml(reviewFile, {\n lineWidth: 0, // Disable line wrapping\n });\n\n // Add schema reference comment at top (local copy alongside reviews.yaml)\n const schemaComment = `# yaml-language-server: $schema=./review.schema.json\\n`;\n\n // Replace empty reviews arrays with commented example\n const reviewsExample = `reviews:\n # - reviewer: human:your-name\n # decision: include # include / exclude / uncertain\n # comment: reason`;\n const finalContent = schemaComment + yamlContent.replace(\n /reviews: \\[\\]/g,\n reviewsExample\n );\n\n // Write reviews.yaml\n await writeFile(reviewsPath, finalContent, 'utf-8');\n\n // Generate and write schema file to .internal/ alongside reviews.yaml\n const schemaDestPath = join(internalDir, 'review.schema.json');\n const jsonSchema = generateReviewJSONSchema();\n await writeFile(schemaDestPath, JSON.stringify(jsonSchema, null, 2) + '\\n', 'utf-8');\n\n return {\n reviewsPath,\n articleCount: articleEntries.length,\n duplicatesRemoved,\n };\n}\n"],"names":["stringifyYaml"],"mappings":"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"}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zod schemas for review workflow types.
|
|
3
|
+
*
|
|
4
|
+
* Single source of truth for:
|
|
5
|
+
* - TypeScript types (via z.infer<>)
|
|
6
|
+
* - JSON Schema for IDE autocompletion (via z.toJSONSchema())
|
|
7
|
+
*
|
|
8
|
+
* Follows the pattern established in src/query/json-schema.ts.
|
|
9
|
+
*/
|
|
10
|
+
import * as z from 'zod';
|
|
11
|
+
export declare const reviewDecisionSchema: z.ZodEnum<{
|
|
12
|
+
exclude: "exclude";
|
|
13
|
+
include: "include";
|
|
14
|
+
uncertain: "uncertain";
|
|
15
|
+
}>;
|
|
16
|
+
export declare const reviewBasisSchema: z.ZodEnum<{
|
|
17
|
+
title: "title";
|
|
18
|
+
abstract: "abstract";
|
|
19
|
+
fulltext: "fulltext";
|
|
20
|
+
}>;
|
|
21
|
+
export declare const reviewSchema: z.ZodObject<{
|
|
22
|
+
reviewer: z.ZodString;
|
|
23
|
+
decision: z.ZodOptional<z.ZodEnum<{
|
|
24
|
+
exclude: "exclude";
|
|
25
|
+
include: "include";
|
|
26
|
+
uncertain: "uncertain";
|
|
27
|
+
}>>;
|
|
28
|
+
basis: z.ZodOptional<z.ZodEnum<{
|
|
29
|
+
title: "title";
|
|
30
|
+
abstract: "abstract";
|
|
31
|
+
fulltext: "fulltext";
|
|
32
|
+
}>>;
|
|
33
|
+
comment: z.ZodOptional<z.ZodString>;
|
|
34
|
+
timestamp: z.ZodOptional<z.ZodString>;
|
|
35
|
+
}, z.core.$strict>;
|
|
36
|
+
export declare const mergedSourceSchema: z.ZodObject<{
|
|
37
|
+
source: z.ZodString;
|
|
38
|
+
pmid: z.ZodOptional<z.ZodString>;
|
|
39
|
+
doi: z.ZodOptional<z.ZodString>;
|
|
40
|
+
scopusId: z.ZodOptional<z.ZodString>;
|
|
41
|
+
arxivId: z.ZodOptional<z.ZodString>;
|
|
42
|
+
ericId: z.ZodOptional<z.ZodString>;
|
|
43
|
+
}, z.core.$strict>;
|
|
44
|
+
export declare const articleFulltextRefSchema: z.ZodObject<{
|
|
45
|
+
dirName: z.ZodString;
|
|
46
|
+
hasFiles: z.ZodObject<{
|
|
47
|
+
pdf: z.ZodBoolean;
|
|
48
|
+
xml: z.ZodBoolean;
|
|
49
|
+
html: z.ZodBoolean;
|
|
50
|
+
markdown: z.ZodBoolean;
|
|
51
|
+
}, z.core.$strict>;
|
|
52
|
+
}, z.core.$strict>;
|
|
53
|
+
export declare const articleEntrySchema: z.ZodObject<{
|
|
54
|
+
doi: z.ZodOptional<z.ZodString>;
|
|
55
|
+
pmid: z.ZodOptional<z.ZodString>;
|
|
56
|
+
scopusId: z.ZodOptional<z.ZodString>;
|
|
57
|
+
arxivId: z.ZodOptional<z.ZodString>;
|
|
58
|
+
ericId: z.ZodOptional<z.ZodString>;
|
|
59
|
+
title: z.ZodString;
|
|
60
|
+
authors: z.ZodOptional<z.ZodString>;
|
|
61
|
+
year: z.ZodOptional<z.ZodString>;
|
|
62
|
+
abstract: z.ZodOptional<z.ZodString>;
|
|
63
|
+
mergedFrom: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
64
|
+
source: z.ZodString;
|
|
65
|
+
pmid: z.ZodOptional<z.ZodString>;
|
|
66
|
+
doi: z.ZodOptional<z.ZodString>;
|
|
67
|
+
scopusId: z.ZodOptional<z.ZodString>;
|
|
68
|
+
arxivId: z.ZodOptional<z.ZodString>;
|
|
69
|
+
ericId: z.ZodOptional<z.ZodString>;
|
|
70
|
+
}, z.core.$strict>>>;
|
|
71
|
+
reviews: z.ZodArray<z.ZodObject<{
|
|
72
|
+
reviewer: z.ZodString;
|
|
73
|
+
decision: z.ZodOptional<z.ZodEnum<{
|
|
74
|
+
exclude: "exclude";
|
|
75
|
+
include: "include";
|
|
76
|
+
uncertain: "uncertain";
|
|
77
|
+
}>>;
|
|
78
|
+
basis: z.ZodOptional<z.ZodEnum<{
|
|
79
|
+
title: "title";
|
|
80
|
+
abstract: "abstract";
|
|
81
|
+
fulltext: "fulltext";
|
|
82
|
+
}>>;
|
|
83
|
+
comment: z.ZodOptional<z.ZodString>;
|
|
84
|
+
timestamp: z.ZodOptional<z.ZodString>;
|
|
85
|
+
}, z.core.$strict>>;
|
|
86
|
+
reviewHistory: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
87
|
+
reviewer: z.ZodString;
|
|
88
|
+
decision: z.ZodOptional<z.ZodEnum<{
|
|
89
|
+
exclude: "exclude";
|
|
90
|
+
include: "include";
|
|
91
|
+
uncertain: "uncertain";
|
|
92
|
+
}>>;
|
|
93
|
+
basis: z.ZodOptional<z.ZodEnum<{
|
|
94
|
+
title: "title";
|
|
95
|
+
abstract: "abstract";
|
|
96
|
+
fulltext: "fulltext";
|
|
97
|
+
}>>;
|
|
98
|
+
comment: z.ZodOptional<z.ZodString>;
|
|
99
|
+
timestamp: z.ZodOptional<z.ZodString>;
|
|
100
|
+
}, z.core.$strict>>>;
|
|
101
|
+
finalDecision: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<"include">, z.ZodLiteral<"exclude">, z.ZodNull]>>;
|
|
102
|
+
fulltext: z.ZodOptional<z.ZodObject<{
|
|
103
|
+
dirName: z.ZodString;
|
|
104
|
+
hasFiles: z.ZodObject<{
|
|
105
|
+
pdf: z.ZodBoolean;
|
|
106
|
+
xml: z.ZodBoolean;
|
|
107
|
+
html: z.ZodBoolean;
|
|
108
|
+
markdown: z.ZodBoolean;
|
|
109
|
+
}, z.core.$strict>;
|
|
110
|
+
}, z.core.$strict>>;
|
|
111
|
+
}, z.core.$strict>;
|
|
112
|
+
export declare const reviewerRecordSchema: z.ZodObject<{
|
|
113
|
+
name: z.ZodString;
|
|
114
|
+
basis: z.ZodEnum<{
|
|
115
|
+
title: "title";
|
|
116
|
+
abstract: "abstract";
|
|
117
|
+
fulltext: "fulltext";
|
|
118
|
+
}>;
|
|
119
|
+
}, z.core.$strict>;
|
|
120
|
+
export declare const reviewFileSchema: z.ZodObject<{
|
|
121
|
+
sessionId: z.ZodString;
|
|
122
|
+
criteria: z.ZodOptional<z.ZodString>;
|
|
123
|
+
reviewer: z.ZodOptional<z.ZodString>;
|
|
124
|
+
basis: z.ZodOptional<z.ZodEnum<{
|
|
125
|
+
title: "title";
|
|
126
|
+
abstract: "abstract";
|
|
127
|
+
fulltext: "fulltext";
|
|
128
|
+
}>>;
|
|
129
|
+
articles: z.ZodArray<z.ZodObject<{
|
|
130
|
+
doi: z.ZodOptional<z.ZodString>;
|
|
131
|
+
pmid: z.ZodOptional<z.ZodString>;
|
|
132
|
+
scopusId: z.ZodOptional<z.ZodString>;
|
|
133
|
+
arxivId: z.ZodOptional<z.ZodString>;
|
|
134
|
+
ericId: z.ZodOptional<z.ZodString>;
|
|
135
|
+
title: z.ZodString;
|
|
136
|
+
authors: z.ZodOptional<z.ZodString>;
|
|
137
|
+
year: z.ZodOptional<z.ZodString>;
|
|
138
|
+
abstract: z.ZodOptional<z.ZodString>;
|
|
139
|
+
mergedFrom: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
140
|
+
source: z.ZodString;
|
|
141
|
+
pmid: z.ZodOptional<z.ZodString>;
|
|
142
|
+
doi: z.ZodOptional<z.ZodString>;
|
|
143
|
+
scopusId: z.ZodOptional<z.ZodString>;
|
|
144
|
+
arxivId: z.ZodOptional<z.ZodString>;
|
|
145
|
+
ericId: z.ZodOptional<z.ZodString>;
|
|
146
|
+
}, z.core.$strict>>>;
|
|
147
|
+
reviews: z.ZodArray<z.ZodObject<{
|
|
148
|
+
reviewer: z.ZodString;
|
|
149
|
+
decision: z.ZodOptional<z.ZodEnum<{
|
|
150
|
+
exclude: "exclude";
|
|
151
|
+
include: "include";
|
|
152
|
+
uncertain: "uncertain";
|
|
153
|
+
}>>;
|
|
154
|
+
basis: z.ZodOptional<z.ZodEnum<{
|
|
155
|
+
title: "title";
|
|
156
|
+
abstract: "abstract";
|
|
157
|
+
fulltext: "fulltext";
|
|
158
|
+
}>>;
|
|
159
|
+
comment: z.ZodOptional<z.ZodString>;
|
|
160
|
+
timestamp: z.ZodOptional<z.ZodString>;
|
|
161
|
+
}, z.core.$strict>>;
|
|
162
|
+
reviewHistory: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
163
|
+
reviewer: z.ZodString;
|
|
164
|
+
decision: z.ZodOptional<z.ZodEnum<{
|
|
165
|
+
exclude: "exclude";
|
|
166
|
+
include: "include";
|
|
167
|
+
uncertain: "uncertain";
|
|
168
|
+
}>>;
|
|
169
|
+
basis: z.ZodOptional<z.ZodEnum<{
|
|
170
|
+
title: "title";
|
|
171
|
+
abstract: "abstract";
|
|
172
|
+
fulltext: "fulltext";
|
|
173
|
+
}>>;
|
|
174
|
+
comment: z.ZodOptional<z.ZodString>;
|
|
175
|
+
timestamp: z.ZodOptional<z.ZodString>;
|
|
176
|
+
}, z.core.$strict>>>;
|
|
177
|
+
finalDecision: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<"include">, z.ZodLiteral<"exclude">, z.ZodNull]>>;
|
|
178
|
+
fulltext: z.ZodOptional<z.ZodObject<{
|
|
179
|
+
dirName: z.ZodString;
|
|
180
|
+
hasFiles: z.ZodObject<{
|
|
181
|
+
pdf: z.ZodBoolean;
|
|
182
|
+
xml: z.ZodBoolean;
|
|
183
|
+
html: z.ZodBoolean;
|
|
184
|
+
markdown: z.ZodBoolean;
|
|
185
|
+
}, z.core.$strict>;
|
|
186
|
+
}, z.core.$strict>>;
|
|
187
|
+
}, z.core.$strict>>;
|
|
188
|
+
reviewers: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
189
|
+
name: z.ZodString;
|
|
190
|
+
basis: z.ZodEnum<{
|
|
191
|
+
title: "title";
|
|
192
|
+
abstract: "abstract";
|
|
193
|
+
fulltext: "fulltext";
|
|
194
|
+
}>;
|
|
195
|
+
}, z.core.$strict>>>;
|
|
196
|
+
}, z.core.$strict>;
|
|
197
|
+
/** Generate a JSON Schema from the review file Zod schema. */
|
|
198
|
+
export declare function generateReviewJSONSchema(): Record<string, unknown>;
|
|
199
|
+
//# sourceMappingURL=schema.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/review/schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAAK,CAAC,MAAM,KAAK,CAAC;AAEzB,eAAO,MAAM,oBAAoB;;;;EAA8C,CAAC;AAEhF,eAAO,MAAM,iBAAiB;;;;EAA4C,CAAC;AAE3E,eAAO,MAAM,YAAY;;;;;;;;;;;;;;kBASyB,CAAC;AAEnD,eAAO,MAAM,kBAAkB;;;;;;;kBAUwB,CAAC;AAExD,eAAO,MAAM,wBAAwB;;;;;;;;kBAY1B,CAAC;AAEZ,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBA0ByC,CAAC;AAEzE,eAAO,MAAM,oBAAoB;;;;;;;kBAM6C,CAAC;AAE/E,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAU6B,CAAC;AAE3D,8DAA8D;AAC9D,wBAAgB,wBAAwB,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAIlE"}
|