@ncukondo/search-hub 0.17.0 → 0.19.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/register.d.ts +1 -1
- package/dist/cli/commands/register.js.map +1 -1
- package/dist/cli/commands/review/finalize.js +6 -6
- package/dist/cli/commands/review/finalize.js.map +1 -1
- 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/list.d.ts +1 -1
- package/dist/cli/commands/review/list.d.ts.map +1 -1
- package/dist/cli/commands/review/list.js.map +1 -1
- package/dist/cli/commands/review/next-steps.d.ts +1 -1
- package/dist/cli/commands/review/next-steps.js +3 -3
- package/dist/cli/commands/review/next-steps.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/status.d.ts +2 -2
- package/dist/cli/commands/review/status.d.ts.map +1 -1
- package/dist/cli/commands/review/status.js +13 -13
- package/dist/cli/commands/review/status.js.map +1 -1
- package/dist/cli/commands/review/types.d.ts +20 -75
- package/dist/cli/commands/review/types.d.ts.map +1 -1
- package/dist/cli/commands/review/types.js +8 -9
- package/dist/cli/commands/review/types.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +78 -4
- 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
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import "zod";
|
|
2
|
+
import "./schema.js";
|
|
1
3
|
const BASIS_RANK = {
|
|
2
4
|
title: 1,
|
|
3
5
|
abstract: 2,
|
|
@@ -67,19 +69,16 @@ function classifyStatus(entry, registeredReviewers) {
|
|
|
67
69
|
if (effectiveDecisions.length === 0) {
|
|
68
70
|
return "pending";
|
|
69
71
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if (hasInclude && hasExclude) {
|
|
73
|
-
return "conflicting";
|
|
74
|
-
}
|
|
75
|
-
const hasUncertain = effectiveDecisions.includes("uncertain");
|
|
76
|
-
if (hasUncertain) {
|
|
77
|
-
return "uncertain";
|
|
72
|
+
if (effectiveDecisions.every((d) => d === "uncertain")) {
|
|
73
|
+
return "all-uncertain";
|
|
78
74
|
}
|
|
79
75
|
if (effectiveDecisions.every((d) => d === "include")) {
|
|
80
76
|
return "agreed-include";
|
|
81
77
|
}
|
|
82
|
-
|
|
78
|
+
if (effectiveDecisions.every((d) => d === "exclude")) {
|
|
79
|
+
return "agreed-exclude";
|
|
80
|
+
}
|
|
81
|
+
return "divided";
|
|
83
82
|
}
|
|
84
83
|
export {
|
|
85
84
|
basisRank,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sources":["../../../../src/cli/commands/review/types.ts"],"sourcesContent":["/**\n * Review workflow types for article assessment tracking\n */\n\nimport type { ArticleFulltextRef } from '@ncukondo/academic-fulltext';\n\nexport type ReviewDecision = 'include' | 'exclude' | 'uncertain';\n\n/**\n * Basis of the review decision (what information was used)\n */\nexport type ReviewBasis = 'title' | 'abstract' | 'fulltext';\n\n/**\n * Individual assessment of an article by a reviewer\n */\nexport interface Review {\n /** Reviewer identifier: \"human:name\" or \"ai:name\" */\n reviewer: string;\n /** Assessment decision */\n decision?: ReviewDecision;\n /** Basis of the decision (what information was used) */\n basis?: ReviewBasis;\n /** Optional comment or reason */\n comment?: string;\n /** ISO 8601 timestamp (optional - auto-assigned on merge if not provided) */\n timestamp?: string;\n}\n\n/**\n * Source information for merged duplicates\n */\nexport interface MergedSource {\n source: string;\n pmid?: string;\n doi?: string;\n scopusId?: string;\n arxivId?: string;\n ericId?: string;\n}\n\n/**\n * Article entry with identifiers, bibliographic info, and reviews\n */\nexport interface ArticleEntry {\n // Identifiers (at least one required for matching)\n doi?: string;\n pmid?: string;\n scopusId?: string;\n arxivId?: string;\n ericId?: string;\n\n // Bibliographic info (for reviewer reference)\n title: string;\n authors?: string;\n year?: string;\n abstract?: string;\n\n // Deduplication tracking\n mergedFrom?: MergedSource[];\n\n // Review data\n reviews: Review[];\n /** Historical reviews (only in extracted ReviewFiles, never in master file) */\n reviewHistory?: Review[];\n finalDecision?: 'include' | 'exclude' | null;\n\n // Fulltext reference (set by fulltext init/sync)\n fulltext?: ArticleFulltextRef;\n}\n\n/**\n * Top-level structure of the reviews.yaml file\n */\nexport interface ReviewerRecord {\n name: string;\n basis: ReviewBasis;\n}\n\nexport interface ReviewFile {\n sessionId: string;\n /** Path to inclusion criteria file */\n criteria?: string;\n /** Reviewer identifier (only in extracted ReviewFiles) */\n reviewer?: string;\n /** Basis level for screening (only in extracted ReviewFiles) */\n basis?: ReviewBasis;\n articles: ArticleEntry[];\n /** Registry of reviewers who participated at each basis level */\n reviewers?: ReviewerRecord[];\n}\n\n/**\n * Work file article entry for AI agent workflow\n */\n/** @deprecated Use ReviewFile format with reviews[] instead. Kept for backward compatibility. */\nexport interface WorkFileArticle {\n id: string;\n title: string;\n abstract?: string;\n /** Fulltext directory name (only for fulltext basis) */\n fulltext?: string;\n decision: ReviewDecision | null;\n comment: string;\n}\n\n/**\n * Work file structure for AI agent workflow\n */\n/** @deprecated Use ReviewFile format with basis field instead. Kept for backward compatibility. */\nexport interface WorkFile {\n sessionId: string;\n basis: ReviewBasis;\n reviewer: string;\n articles: WorkFileArticle[];\n}\n\n/**\n * Basis priority rank: fulltext > abstract > title > undefined\n */\nconst BASIS_RANK: Record<string, number> = {\n title: 1,\n abstract: 2,\n fulltext: 3,\n};\n\nexport function basisRank(basis: ReviewBasis | undefined): number {\n if (basis === undefined) return 0;\n return BASIS_RANK[basis] ?? 0;\n}\n\n/**\n * Review status classification (7-state model)\n */\nexport type ReviewStatus =\n | 'pending'\n | 'incomplete'\n | 'uncertain'\n | 'agreed-include'\n | 'agreed-exclude'\n | 'conflicting'\n | 'finalized';\n\n/**\n * Classify the review status of an article entry\n *\n * Classification logic (in order):\n * 1. finalDecision set? → finalized\n * 2. No reviews? → pending\n * 3. Registered reviewer missing? → incomplete\n * 4. include AND exclude present? → conflicting\n * 5. Any uncertain? → uncertain\n * 6. All include? → agreed-include\n * 7. All exclude? → agreed-exclude\n */\nexport function classifyStatus(\n entry: ArticleEntry,\n registeredReviewers?: ReviewerRecord[]\n): ReviewStatus {\n // 1. Finalized takes precedence\n if (entry.finalDecision !== undefined && entry.finalDecision !== null) {\n return 'finalized';\n }\n\n // No reviews = pending (reviews can be null from YAML parsing with only comments)\n const reviews = entry.reviews ?? [];\n if (reviews.length === 0) {\n return 'pending';\n }\n\n // 3. Check for incomplete (registered reviewer missing)\n // Only check reviewers whose registered basis ≤ article's highest reviewed basis\n if (registeredReviewers && registeredReviewers.length > 0) {\n const reviewerNames = new Set(reviews.map((r) => r.reviewer));\n let highestReviewedRank = 0;\n for (const r of reviews) {\n highestReviewedRank = Math.max(highestReviewedRank, basisRank(r.basis));\n }\n // When reviews have no basis (legacy), check all registered reviewers\n const applicableReviewers = highestReviewedRank === 0\n ? registeredReviewers\n : registeredReviewers.filter(\n (reg) => basisRank(reg.basis) <= highestReviewedRank\n );\n const hasAllReviewers = applicableReviewers.every((reg) =>\n reviewerNames.has(reg.name)\n );\n if (applicableReviewers.length > 0 && !hasAllReviewers) {\n return 'incomplete';\n }\n }\n\n // Get reviews that have decisions\n const reviewsWithDecisions = reviews.filter((r) => r.decision !== undefined);\n\n if (reviewsWithDecisions.length === 0) {\n // All reviews lack a decision — treat as pending\n return 'pending';\n }\n\n // Basis-priority resolution:\n // \"uncertain\" at a lower basis means \"need more info\" (escalate).\n // A definitive decision at a higher basis resolves that uncertainty.\n //\n // Algorithm:\n // 1. Find the highest basis rank among all definitive (include/exclude) reviews\n // 2. For each reviewer, compute their effective decision:\n // - Take their highest-basis definitive decision if they have one\n // - Otherwise, keep uncertain only if their uncertain rank >= highest definitive rank\n // (i.e., no higher-basis definitive exists globally to resolve it)\n // 3. Reviewers whose only reviews are uncertain at a lower basis than the\n // highest global definitive are excluded from consensus (their uncertainty was resolved)\n\n // Find highest definitive basis rank across ALL reviews\n let highestDefinitiveRank = 0;\n for (const r of reviewsWithDecisions) {\n if (r.decision !== 'uncertain') {\n highestDefinitiveRank = Math.max(highestDefinitiveRank, basisRank(r.basis));\n }\n }\n\n // For each reviewer, compute effective decision\n const reviewerMap = new Map<string, { decision: ReviewDecision; rank: number }>();\n for (const r of reviewsWithDecisions) {\n const rank = basisRank(r.basis);\n const existing = reviewerMap.get(r.reviewer);\n if (!existing) {\n reviewerMap.set(r.reviewer, { decision: r.decision!, rank });\n } else {\n // Prefer definitive over uncertain\n if (r.decision !== 'uncertain' && existing.decision === 'uncertain') {\n reviewerMap.set(r.reviewer, { decision: r.decision!, rank });\n } else if (r.decision !== 'uncertain' && existing.decision !== 'uncertain' && rank > existing.rank) {\n // Higher-basis definitive overrides lower-basis definitive\n reviewerMap.set(r.reviewer, { decision: r.decision!, rank });\n } else if (r.decision === 'uncertain' && existing.decision === 'uncertain' && rank > existing.rank) {\n reviewerMap.set(r.reviewer, { decision: r.decision!, rank });\n }\n }\n }\n\n // Collect effective decisions, excluding reviewers whose effective decision\n // is at a lower basis than the highest global definitive\n const effectiveDecisions: ReviewDecision[] = [];\n for (const { decision, rank } of reviewerMap.values()) {\n if (rank < highestDefinitiveRank) {\n // This reviewer's decision is at a lower basis than the highest definitive — skip\n continue;\n }\n effectiveDecisions.push(decision);\n }\n\n if (effectiveDecisions.length === 0) {\n return 'pending';\n }\n\n // 4. Check for conflicts: both include and exclude present among effective decisions\n const hasInclude = effectiveDecisions.includes('include');\n const hasExclude = effectiveDecisions.includes('exclude');\n if (hasInclude && hasExclude) {\n return 'conflicting';\n }\n\n // 5. Any effective uncertain?\n const hasUncertain = effectiveDecisions.includes('uncertain');\n if (hasUncertain) {\n return 'uncertain';\n }\n\n // 6. All include?\n if (effectiveDecisions.every((d) => d === 'include')) {\n return 'agreed-include';\n }\n\n // 7. All exclude (only remaining possibility after ruling out conflicts and uncertain)\n return 'agreed-exclude';\n}\n"],"names":[],"mappings":"AAwHA,MAAM,aAAqC;AAAA,EACzC,OAAO;AAAA,EACP,UAAU;AAAA,EACV,UAAU;AACZ;AAEO,SAAS,UAAU,OAAwC;AAChE,MAAI,UAAU,OAAW,QAAO;AAChC,SAAO,WAAW,KAAK,KAAK;AAC9B;AA0BO,SAAS,eACd,OACA,qBACc;AAEd,MAAI,MAAM,kBAAkB,UAAa,MAAM,kBAAkB,MAAM;AACrE,WAAO;AAAA,EACT;AAGA,QAAM,UAAU,MAAM,WAAW,CAAA;AACjC,MAAI,QAAQ,WAAW,GAAG;AACxB,WAAO;AAAA,EACT;AAIA,MAAI,uBAAuB,oBAAoB,SAAS,GAAG;AACzD,UAAM,gBAAgB,IAAI,IAAI,QAAQ,IAAI,CAAC,MAAM,EAAE,QAAQ,CAAC;AAC5D,QAAI,sBAAsB;AAC1B,eAAW,KAAK,SAAS;AACvB,4BAAsB,KAAK,IAAI,qBAAqB,UAAU,EAAE,KAAK,CAAC;AAAA,IACxE;AAEA,UAAM,sBAAsB,wBAAwB,IAChD,sBACA,oBAAoB;AAAA,MAClB,CAAC,QAAQ,UAAU,IAAI,KAAK,KAAK;AAAA,IAAA;AAEvC,UAAM,kBAAkB,oBAAoB;AAAA,MAAM,CAAC,QACjD,cAAc,IAAI,IAAI,IAAI;AAAA,IAAA;AAE5B,QAAI,oBAAoB,SAAS,KAAK,CAAC,iBAAiB;AACtD,aAAO;AAAA,IACT;AAAA,EACF;AAGA,QAAM,uBAAuB,QAAQ,OAAO,CAAC,MAAM,EAAE,aAAa,MAAS;AAE3E,MAAI,qBAAqB,WAAW,GAAG;AAErC,WAAO;AAAA,EACT;AAgBA,MAAI,wBAAwB;AAC5B,aAAW,KAAK,sBAAsB;AACpC,QAAI,EAAE,aAAa,aAAa;AAC9B,8BAAwB,KAAK,IAAI,uBAAuB,UAAU,EAAE,KAAK,CAAC;AAAA,IAC5E;AAAA,EACF;AAGA,QAAM,kCAAkB,IAAA;AACxB,aAAW,KAAK,sBAAsB;AACpC,UAAM,OAAO,UAAU,EAAE,KAAK;AAC9B,UAAM,WAAW,YAAY,IAAI,EAAE,QAAQ;AAC3C,QAAI,CAAC,UAAU;AACb,kBAAY,IAAI,EAAE,UAAU,EAAE,UAAU,EAAE,UAAW,MAAM;AAAA,IAC7D,OAAO;AAEL,UAAI,EAAE,aAAa,eAAe,SAAS,aAAa,aAAa;AACnE,oBAAY,IAAI,EAAE,UAAU,EAAE,UAAU,EAAE,UAAW,MAAM;AAAA,MAC7D,WAAW,EAAE,aAAa,eAAe,SAAS,aAAa,eAAe,OAAO,SAAS,MAAM;AAElG,oBAAY,IAAI,EAAE,UAAU,EAAE,UAAU,EAAE,UAAW,MAAM;AAAA,MAC7D,WAAW,EAAE,aAAa,eAAe,SAAS,aAAa,eAAe,OAAO,SAAS,MAAM;AAClG,oBAAY,IAAI,EAAE,UAAU,EAAE,UAAU,EAAE,UAAW,MAAM;AAAA,MAC7D;AAAA,IACF;AAAA,EACF;AAIA,QAAM,qBAAuC,CAAA;AAC7C,aAAW,EAAE,UAAU,KAAA,KAAU,YAAY,UAAU;AACrD,QAAI,OAAO,uBAAuB;AAEhC;AAAA,IACF;AACA,uBAAmB,KAAK,QAAQ;AAAA,EAClC;AAEA,MAAI,mBAAmB,WAAW,GAAG;AACnC,WAAO;AAAA,EACT;AAGA,QAAM,aAAa,mBAAmB,SAAS,SAAS;AACxD,QAAM,aAAa,mBAAmB,SAAS,SAAS;AACxD,MAAI,cAAc,YAAY;AAC5B,WAAO;AAAA,EACT;AAGA,QAAM,eAAe,mBAAmB,SAAS,WAAW;AAC5D,MAAI,cAAc;AAChB,WAAO;AAAA,EACT;AAGA,MAAI,mBAAmB,MAAM,CAAC,MAAM,MAAM,SAAS,GAAG;AACpD,WAAO;AAAA,EACT;AAGA,SAAO;AACT;"}
|
|
1
|
+
{"version":3,"file":"types.js","sources":["../../../../src/cli/commands/review/types.ts"],"sourcesContent":["/**\n * Review workflow types for article assessment tracking.\n *\n * Core data types (ReviewDecision, ReviewBasis, Review, MergedSource,\n * ArticleEntry, ReviewerRecord, ReviewFile) are derived from Zod schemas\n * in schema.ts — single source of truth for types and JSON Schema.\n */\nimport * as z from 'zod';\nimport {\n reviewDecisionSchema,\n reviewBasisSchema,\n reviewSchema,\n mergedSourceSchema,\n articleEntrySchema,\n reviewerRecordSchema,\n reviewFileSchema,\n} from './schema.js';\n\nexport type ReviewDecision = z.infer<typeof reviewDecisionSchema>;\nexport type ReviewBasis = z.infer<typeof reviewBasisSchema>;\nexport type Review = z.infer<typeof reviewSchema>;\nexport type MergedSource = z.infer<typeof mergedSourceSchema>;\nexport type ArticleEntry = z.infer<typeof articleEntrySchema>;\nexport type ReviewerRecord = z.infer<typeof reviewerRecordSchema>;\nexport type ReviewFile = z.infer<typeof reviewFileSchema>;\n\n/**\n * Work file article entry for AI agent workflow\n */\n/** @deprecated Use ReviewFile format with reviews[] instead. Kept for backward compatibility. */\nexport interface WorkFileArticle {\n id: string;\n title: string;\n abstract?: string;\n /** Fulltext directory name (only for fulltext basis) */\n fulltext?: string;\n decision: ReviewDecision | null;\n comment: string;\n}\n\n/**\n * Work file structure for AI agent workflow\n */\n/** @deprecated Use ReviewFile format with basis field instead. Kept for backward compatibility. */\nexport interface WorkFile {\n sessionId: string;\n basis: ReviewBasis;\n reviewer: string;\n articles: WorkFileArticle[];\n}\n\n/**\n * Basis priority rank: fulltext > abstract > title > undefined\n */\nconst BASIS_RANK: Record<string, number> = {\n title: 1,\n abstract: 2,\n fulltext: 3,\n};\n\nexport function basisRank(basis: ReviewBasis | undefined): number {\n if (basis === undefined) return 0;\n return BASIS_RANK[basis] ?? 0;\n}\n\n/**\n * Review status classification (7-state model)\n */\nexport type ReviewStatus =\n | 'pending'\n | 'incomplete'\n | 'all-uncertain'\n | 'agreed-include'\n | 'agreed-exclude'\n | 'divided'\n | 'finalized';\n\n/**\n * Classify the review status of an article entry\n *\n * Classification logic (in order):\n * 1. finalDecision set? → finalized\n * 2. No reviews? → pending\n * 3. Registered reviewer missing? → incomplete\n * 4. All uncertain? → all-uncertain\n * 5. All include? → agreed-include\n * 6. All exclude? → agreed-exclude\n * 7. Any mix of decisions? → divided\n */\nexport function classifyStatus(\n entry: ArticleEntry,\n registeredReviewers?: ReviewerRecord[]\n): ReviewStatus {\n // 1. Finalized takes precedence\n if (entry.finalDecision !== undefined && entry.finalDecision !== null) {\n return 'finalized';\n }\n\n // No reviews = pending (reviews can be null from YAML parsing with only comments)\n const reviews = entry.reviews ?? [];\n if (reviews.length === 0) {\n return 'pending';\n }\n\n // 3. Check for incomplete (registered reviewer missing)\n // Only check reviewers whose registered basis ≤ article's highest reviewed basis\n if (registeredReviewers && registeredReviewers.length > 0) {\n const reviewerNames = new Set(reviews.map((r) => r.reviewer));\n let highestReviewedRank = 0;\n for (const r of reviews) {\n highestReviewedRank = Math.max(highestReviewedRank, basisRank(r.basis));\n }\n // When reviews have no basis (legacy), check all registered reviewers\n const applicableReviewers = highestReviewedRank === 0\n ? registeredReviewers\n : registeredReviewers.filter(\n (reg) => basisRank(reg.basis) <= highestReviewedRank\n );\n const hasAllReviewers = applicableReviewers.every((reg) =>\n reviewerNames.has(reg.name)\n );\n if (applicableReviewers.length > 0 && !hasAllReviewers) {\n return 'incomplete';\n }\n }\n\n // Get reviews that have decisions\n const reviewsWithDecisions = reviews.filter((r) => r.decision !== undefined);\n\n if (reviewsWithDecisions.length === 0) {\n // All reviews lack a decision — treat as pending\n return 'pending';\n }\n\n // Basis-priority resolution:\n // \"uncertain\" at a lower basis means \"need more info\" (escalate).\n // A definitive decision at a higher basis resolves that uncertainty.\n //\n // Algorithm:\n // 1. Find the highest basis rank among all definitive (include/exclude) reviews\n // 2. For each reviewer, compute their effective decision:\n // - Take their highest-basis definitive decision if they have one\n // - Otherwise, keep uncertain only if their uncertain rank >= highest definitive rank\n // (i.e., no higher-basis definitive exists globally to resolve it)\n // 3. Reviewers whose only reviews are uncertain at a lower basis than the\n // highest global definitive are excluded from consensus (their uncertainty was resolved)\n\n // Find highest definitive basis rank across ALL reviews\n let highestDefinitiveRank = 0;\n for (const r of reviewsWithDecisions) {\n if (r.decision !== 'uncertain') {\n highestDefinitiveRank = Math.max(highestDefinitiveRank, basisRank(r.basis));\n }\n }\n\n // For each reviewer, compute effective decision\n const reviewerMap = new Map<string, { decision: ReviewDecision; rank: number }>();\n for (const r of reviewsWithDecisions) {\n const rank = basisRank(r.basis);\n const existing = reviewerMap.get(r.reviewer);\n if (!existing) {\n reviewerMap.set(r.reviewer, { decision: r.decision!, rank });\n } else {\n // Prefer definitive over uncertain\n if (r.decision !== 'uncertain' && existing.decision === 'uncertain') {\n reviewerMap.set(r.reviewer, { decision: r.decision!, rank });\n } else if (r.decision !== 'uncertain' && existing.decision !== 'uncertain' && rank > existing.rank) {\n // Higher-basis definitive overrides lower-basis definitive\n reviewerMap.set(r.reviewer, { decision: r.decision!, rank });\n } else if (r.decision === 'uncertain' && existing.decision === 'uncertain' && rank > existing.rank) {\n reviewerMap.set(r.reviewer, { decision: r.decision!, rank });\n }\n }\n }\n\n // Collect effective decisions, excluding reviewers whose effective decision\n // is at a lower basis than the highest global definitive\n const effectiveDecisions: ReviewDecision[] = [];\n for (const { decision, rank } of reviewerMap.values()) {\n if (rank < highestDefinitiveRank) {\n // This reviewer's decision is at a lower basis than the highest definitive — skip\n continue;\n }\n effectiveDecisions.push(decision);\n }\n\n if (effectiveDecisions.length === 0) {\n return 'pending';\n }\n\n // 4. Check all-uncertain: every effective decision is uncertain\n if (effectiveDecisions.every((d) => d === 'uncertain')) {\n return 'all-uncertain';\n }\n\n // 5. All include?\n if (effectiveDecisions.every((d) => d === 'include')) {\n return 'agreed-include';\n }\n\n // 6. All exclude?\n if (effectiveDecisions.every((d) => d === 'exclude')) {\n return 'agreed-exclude';\n }\n\n // 7. Any mix of different decisions → divided\n return 'divided';\n}\n"],"names":[],"mappings":";;AAsDA,MAAM,aAAqC;AAAA,EACzC,OAAO;AAAA,EACP,UAAU;AAAA,EACV,UAAU;AACZ;AAEO,SAAS,UAAU,OAAwC;AAChE,MAAI,UAAU,OAAW,QAAO;AAChC,SAAO,WAAW,KAAK,KAAK;AAC9B;AA0BO,SAAS,eACd,OACA,qBACc;AAEd,MAAI,MAAM,kBAAkB,UAAa,MAAM,kBAAkB,MAAM;AACrE,WAAO;AAAA,EACT;AAGA,QAAM,UAAU,MAAM,WAAW,CAAA;AACjC,MAAI,QAAQ,WAAW,GAAG;AACxB,WAAO;AAAA,EACT;AAIA,MAAI,uBAAuB,oBAAoB,SAAS,GAAG;AACzD,UAAM,gBAAgB,IAAI,IAAI,QAAQ,IAAI,CAAC,MAAM,EAAE,QAAQ,CAAC;AAC5D,QAAI,sBAAsB;AAC1B,eAAW,KAAK,SAAS;AACvB,4BAAsB,KAAK,IAAI,qBAAqB,UAAU,EAAE,KAAK,CAAC;AAAA,IACxE;AAEA,UAAM,sBAAsB,wBAAwB,IAChD,sBACA,oBAAoB;AAAA,MAClB,CAAC,QAAQ,UAAU,IAAI,KAAK,KAAK;AAAA,IAAA;AAEvC,UAAM,kBAAkB,oBAAoB;AAAA,MAAM,CAAC,QACjD,cAAc,IAAI,IAAI,IAAI;AAAA,IAAA;AAE5B,QAAI,oBAAoB,SAAS,KAAK,CAAC,iBAAiB;AACtD,aAAO;AAAA,IACT;AAAA,EACF;AAGA,QAAM,uBAAuB,QAAQ,OAAO,CAAC,MAAM,EAAE,aAAa,MAAS;AAE3E,MAAI,qBAAqB,WAAW,GAAG;AAErC,WAAO;AAAA,EACT;AAgBA,MAAI,wBAAwB;AAC5B,aAAW,KAAK,sBAAsB;AACpC,QAAI,EAAE,aAAa,aAAa;AAC9B,8BAAwB,KAAK,IAAI,uBAAuB,UAAU,EAAE,KAAK,CAAC;AAAA,IAC5E;AAAA,EACF;AAGA,QAAM,kCAAkB,IAAA;AACxB,aAAW,KAAK,sBAAsB;AACpC,UAAM,OAAO,UAAU,EAAE,KAAK;AAC9B,UAAM,WAAW,YAAY,IAAI,EAAE,QAAQ;AAC3C,QAAI,CAAC,UAAU;AACb,kBAAY,IAAI,EAAE,UAAU,EAAE,UAAU,EAAE,UAAW,MAAM;AAAA,IAC7D,OAAO;AAEL,UAAI,EAAE,aAAa,eAAe,SAAS,aAAa,aAAa;AACnE,oBAAY,IAAI,EAAE,UAAU,EAAE,UAAU,EAAE,UAAW,MAAM;AAAA,MAC7D,WAAW,EAAE,aAAa,eAAe,SAAS,aAAa,eAAe,OAAO,SAAS,MAAM;AAElG,oBAAY,IAAI,EAAE,UAAU,EAAE,UAAU,EAAE,UAAW,MAAM;AAAA,MAC7D,WAAW,EAAE,aAAa,eAAe,SAAS,aAAa,eAAe,OAAO,SAAS,MAAM;AAClG,oBAAY,IAAI,EAAE,UAAU,EAAE,UAAU,EAAE,UAAW,MAAM;AAAA,MAC7D;AAAA,IACF;AAAA,EACF;AAIA,QAAM,qBAAuC,CAAA;AAC7C,aAAW,EAAE,UAAU,KAAA,KAAU,YAAY,UAAU;AACrD,QAAI,OAAO,uBAAuB;AAEhC;AAAA,IACF;AACA,uBAAmB,KAAK,QAAQ;AAAA,EAClC;AAEA,MAAI,mBAAmB,WAAW,GAAG;AACnC,WAAO;AAAA,EACT;AAGA,MAAI,mBAAmB,MAAM,CAAC,MAAM,MAAM,WAAW,GAAG;AACtD,WAAO;AAAA,EACT;AAGA,MAAI,mBAAmB,MAAM,CAAC,MAAM,MAAM,SAAS,GAAG;AACpD,WAAO;AAAA,EACT;AAGA,MAAI,mBAAmB,MAAM,CAAC,MAAM,MAAM,SAAS,GAAG;AACpD,WAAO;AAAA,EACT;AAGA,SAAO;AACT;"}
|
package/dist/cli/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":";AAQA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":";AAQA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA8LpC;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,0BAA0B;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gCAAgC;IAChC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,4BAA4B;IAC5B,OAAO,EAAE,OAAO,CAAC;IACjB,wCAAwC;IACxC,KAAK,EAAE,OAAO,CAAC;IACf,qEAAqE;IACrE,KAAK,EAAE,OAAO,CAAC;CAChB;AAED;;GAEG;AACH,wBAAgB,aAAa,IAAI,OAAO,CAkjFvC;AAED;;GAEG;AACH,wBAAsB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAG1C"}
|
package/dist/cli/index.js
CHANGED
|
@@ -38,15 +38,20 @@ import { executeReviewMerge, formatMergeOutput as formatMergeOutput$1 } from "./
|
|
|
38
38
|
import { executeReviewMark } from "./commands/review/mark.js";
|
|
39
39
|
import { executeReviewExport, formatExportOutput } from "./commands/review/export.js";
|
|
40
40
|
import { executeReviewFinalize, formatFinalizeOutput } from "./commands/review/finalize.js";
|
|
41
|
+
import "zod";
|
|
42
|
+
import "./commands/review/schema.js";
|
|
41
43
|
import { registerFulltextCommands } from "./commands/fulltext/index.js";
|
|
42
44
|
import { parseRegisterOptions, validateRegisterInput, hasReviewFile, getReviewSummary, formatNoIncludedArticlesError, formatPendingWarning, confirmPrompt, getIncludedArticles, formatReviewRequiredMessage, formatIgnoringReviewsNote, formatDryRunOutput as formatDryRunOutput$1, formatRegistrationSummary } from "./commands/register.js";
|
|
43
45
|
import { formatSuggestion } from "./suggestions/index.js";
|
|
44
46
|
import { getSuggestion } from "./suggestions/rules.js";
|
|
47
|
+
import { readLogEntries, computeQueryHash, appendLogEntry, buildPreviewLogEntry, buildCountLogEntry } from "./commands/query/iteration-log.js";
|
|
48
|
+
import { executeQueryAssess } from "./commands/query/assess.js";
|
|
49
|
+
import { formatLogOutput } from "./commands/query/log.js";
|
|
45
50
|
import { registerArticles, saveRegistrationRecord } from "../integration/register.js";
|
|
46
51
|
import { checkRefAvailable, checkNpmAvailable, installRefManager } from "../integration/ref-cli.js";
|
|
47
52
|
import { loadSession, listSessions, sessionExists } from "../session/manager.js";
|
|
48
53
|
import { parseQueryFile, detectShortKeywords } from "../query/parser.js";
|
|
49
|
-
import {
|
|
54
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
50
55
|
import { realpathSync } from "node:fs";
|
|
51
56
|
import { join } from "node:path";
|
|
52
57
|
import { fileURLToPath } from "node:url";
|
|
@@ -346,6 +351,54 @@ Examples:
|
|
|
346
351
|
process.exitCode = EXIT_CODES.GENERAL_ERROR;
|
|
347
352
|
}
|
|
348
353
|
});
|
|
354
|
+
queryCommand.command("assess").description("Record an assessment of the current query iteration").argument("<file>", "path to query YAML file").option("--verdict <verdict>", "assessment verdict (e.g., reject, good, refine)").option("--precision <precision>", "estimated precision (e.g., ~60%)").option("--comment <comment>", "free-text comment").addHelpText("after", `
|
|
355
|
+
Examples:
|
|
356
|
+
$ search-hub query assess query.yaml --verdict reject --comment "Too broad"
|
|
357
|
+
$ search-hub query assess query.yaml --verdict good --precision "~60%"`).action(async (file, options) => {
|
|
358
|
+
const globalOpts = program.opts();
|
|
359
|
+
try {
|
|
360
|
+
const result = await executeQueryAssess(file, options);
|
|
361
|
+
if (result.success) {
|
|
362
|
+
if (!globalOpts.quiet) {
|
|
363
|
+
console.log("Assessment recorded.");
|
|
364
|
+
const suggestion = formatSuggestion(getSuggestion({
|
|
365
|
+
command: "query assess",
|
|
366
|
+
queryFile: file
|
|
367
|
+
}));
|
|
368
|
+
if (suggestion) console.log(suggestion);
|
|
369
|
+
}
|
|
370
|
+
process.exitCode = EXIT_CODES.SUCCESS;
|
|
371
|
+
} else {
|
|
372
|
+
if (!globalOpts.quiet) {
|
|
373
|
+
console.error(`Error: ${result.error}`);
|
|
374
|
+
}
|
|
375
|
+
process.exitCode = EXIT_CODES.GENERAL_ERROR;
|
|
376
|
+
}
|
|
377
|
+
} catch (error) {
|
|
378
|
+
if (!globalOpts.quiet) {
|
|
379
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
380
|
+
}
|
|
381
|
+
process.exitCode = EXIT_CODES.GENERAL_ERROR;
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
queryCommand.command("log").description("View the query iteration history").argument("<file>", "path to query YAML file").option("--json", "output as JSON").addHelpText("after", `
|
|
385
|
+
Examples:
|
|
386
|
+
$ search-hub query log query.yaml
|
|
387
|
+
$ search-hub query log query.yaml --json`).action(async (file, options) => {
|
|
388
|
+
const globalOpts = program.opts();
|
|
389
|
+
try {
|
|
390
|
+
const entries = await readLogEntries(file);
|
|
391
|
+
if (!globalOpts.quiet) {
|
|
392
|
+
console.log(formatLogOutput(entries, { json: options?.json }));
|
|
393
|
+
}
|
|
394
|
+
process.exitCode = EXIT_CODES.SUCCESS;
|
|
395
|
+
} catch (error) {
|
|
396
|
+
if (!globalOpts.quiet) {
|
|
397
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
398
|
+
}
|
|
399
|
+
process.exitCode = EXIT_CODES.GENERAL_ERROR;
|
|
400
|
+
}
|
|
401
|
+
});
|
|
349
402
|
program.command("status").description("Show session status and statistics").argument("[session-id]", "session ID to show details for").option("--json", "output as JSON").option("--all", "include completed sessions").addHelpText("after", `
|
|
350
403
|
Examples:
|
|
351
404
|
$ search-hub status # List recent sessions
|
|
@@ -501,8 +554,21 @@ Query features (use "query init" to see full template):
|
|
|
501
554
|
process.exitCode = EXIT_CODES.GENERAL_ERROR;
|
|
502
555
|
return;
|
|
503
556
|
}
|
|
557
|
+
if (searchOpts.queryFile) {
|
|
558
|
+
try {
|
|
559
|
+
const qContent = await readFile(searchOpts.queryFile, "utf-8");
|
|
560
|
+
const qHash = computeQueryHash(qContent);
|
|
561
|
+
await appendLogEntry(searchOpts.queryFile, buildPreviewLogEntry(qHash, previews));
|
|
562
|
+
} catch {
|
|
563
|
+
}
|
|
564
|
+
}
|
|
504
565
|
if (!globalOpts.quiet) {
|
|
505
566
|
console.log(formatPreviewOutput(previews, searchOpts.queryFile));
|
|
567
|
+
const suggestion = formatSuggestion(getSuggestion({
|
|
568
|
+
command: "search --preview",
|
|
569
|
+
queryFile: searchOpts.queryFile
|
|
570
|
+
}));
|
|
571
|
+
if (suggestion) console.log(suggestion);
|
|
506
572
|
}
|
|
507
573
|
const previewHasErrors = previews.some((p) => p.error);
|
|
508
574
|
const previewAllFailed = previews.every((p) => p.error);
|
|
@@ -536,6 +602,14 @@ Warning: Some providers failed:
|
|
|
536
602
|
process.exitCode = EXIT_CODES.GENERAL_ERROR;
|
|
537
603
|
return;
|
|
538
604
|
}
|
|
605
|
+
if (searchOpts.queryFile) {
|
|
606
|
+
try {
|
|
607
|
+
const qContent = await readFile(searchOpts.queryFile, "utf-8");
|
|
608
|
+
const qHash = computeQueryHash(qContent);
|
|
609
|
+
await appendLogEntry(searchOpts.queryFile, buildCountLogEntry(qHash, counts));
|
|
610
|
+
} catch {
|
|
611
|
+
}
|
|
612
|
+
}
|
|
539
613
|
if (!globalOpts.quiet) {
|
|
540
614
|
console.log(formatCountOnlyOutput(counts, searchOpts.queryFile));
|
|
541
615
|
const suggestion = formatSuggestion(getSuggestion({
|
|
@@ -1574,10 +1648,10 @@ Examples:
|
|
|
1574
1648
|
process.exitCode = EXIT_CODES.SESSION_ERROR;
|
|
1575
1649
|
}
|
|
1576
1650
|
});
|
|
1577
|
-
reviewCommand.command("list").description("List articles with optional filtering").requiredOption("--session <id>", "session ID").option("--filter <type>", "filter by status: pending, incomplete, uncertain, agreed-include, agreed-exclude,
|
|
1651
|
+
reviewCommand.command("list").description("List articles with optional filtering").requiredOption("--session <id>", "session ID").option("--filter <type>", "filter by status: pending, incomplete, all-uncertain, agreed-include, agreed-exclude, divided, finalized, all", "all").option("--json", "output as JSON").action(async (options) => {
|
|
1578
1652
|
const globalOpts = program.opts();
|
|
1579
1653
|
try {
|
|
1580
|
-
const validFilters = ["pending", "incomplete", "uncertain", "agreed-include", "agreed-exclude", "
|
|
1654
|
+
const validFilters = ["pending", "incomplete", "all-uncertain", "agreed-include", "agreed-exclude", "divided", "finalized", "all"];
|
|
1581
1655
|
const filter = options.filter ?? "all";
|
|
1582
1656
|
if (!validFilters.includes(filter)) {
|
|
1583
1657
|
if (!globalOpts.quiet) {
|
|
@@ -1607,7 +1681,7 @@ Examples:
|
|
|
1607
1681
|
process.exitCode = EXIT_CODES.SESSION_ERROR;
|
|
1608
1682
|
}
|
|
1609
1683
|
});
|
|
1610
|
-
reviewCommand.command("extract").description("Extract subset to for-review/<name>/review.yaml for distributed review").requiredOption("--session <id>", "session ID").requiredOption("--name <name>", "name for the review subset (output: for-review/<name>/review.yaml)").option("--filter <types>", "filter by status (comma-separated): pending, incomplete, uncertain, agreed-include, agreed-exclude,
|
|
1684
|
+
reviewCommand.command("extract").description("Extract subset to for-review/<name>/review.yaml for distributed review").requiredOption("--session <id>", "session ID").requiredOption("--name <name>", "name for the review subset (output: for-review/<name>/review.yaml)").option("--filter <types>", "filter by status (comma-separated): pending, incomplete, all-uncertain, agreed-include, agreed-exclude, divided, finalized").option("--sort <method>", "sort method: year, title, random, none", "none").option("--limit <n>", "limit number of articles").option("--offset <n>", "skip first n articles").option("--seed <n>", "random seed for reproducible sorting").option("--basis <type>", "basis for review: title, abstract, or fulltext").option("--reviewer <id>", 'reviewer identifier (e.g., "ai:claude")').option("--finalize", "extract for final decision (includes reviewHistory and finalDecision)").action(async (options) => {
|
|
1611
1685
|
const globalOpts = program.opts();
|
|
1612
1686
|
try {
|
|
1613
1687
|
const validSorts = ["year", "title", "random", "none"];
|