@longtable/cli 0.1.44 → 0.1.47
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 +6 -5
- package/dist/cli.js +372 -93
- package/dist/longtable-codex-native-hook.js +8 -2
- package/dist/project-session.d.ts +7 -1
- package/dist/project-session.js +209 -37
- package/dist/search/publisher-access.d.ts +1 -3
- package/dist/search/publisher-access.js +0 -13
- package/dist/search/types.d.ts +0 -6
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -269,16 +269,17 @@ deduplicates, ranks, and labels results as evidence cards. Some sources work
|
|
|
269
269
|
without keys, some require a contact email, and some need API keys for reliable
|
|
270
270
|
use.
|
|
271
271
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
272
|
+
Scholarly access is configured separately through `longtable access setup`.
|
|
273
|
+
It records readiness for metadata, OA full text, institutional access,
|
|
274
|
+
publisher API/TDM credentials, and manual PDFs without storing secrets.
|
|
275
|
+
Publisher probes cover Elsevier, Springer Nature, Wiley, and Taylor & Francis.
|
|
275
276
|
|
|
276
277
|
Citation support should be checked explicitly. A reference can be useful as
|
|
277
278
|
background while still failing to support the specific claim attached to it.
|
|
278
279
|
|
|
279
280
|
```bash
|
|
280
|
-
longtable
|
|
281
|
-
longtable
|
|
281
|
+
longtable access setup
|
|
282
|
+
longtable access probe --doi "10.1016/example" --publisher elsevier
|
|
282
283
|
longtable search --query "trust calibration measurement" --intent measurement
|
|
283
284
|
longtable search --query "trust calibration measurement" --publisher-access --json
|
|
284
285
|
longtable search --query "trust calibration citation support" --intent citation --record
|
package/dist/cli.js
CHANGED
|
@@ -9,7 +9,7 @@ import { dirname, join, resolve } from "node:path";
|
|
|
9
9
|
import { homedir } from "node:os";
|
|
10
10
|
import { fileURLToPath } from "node:url";
|
|
11
11
|
import { classifyCheckpointTrigger } from "@longtable/checkpoints";
|
|
12
|
-
import { assessSearchSourceCapabilities, buildResearchSearchIntent,
|
|
12
|
+
import { assessSearchSourceCapabilities, buildResearchSearchIntent, parsePublisherTarget, probePublisherAccess, publisherConfigs, runResearchSearch, SEARCH_SOURCES, summarizeConfiguredPublisherAccess } from "./search/index.js";
|
|
13
13
|
import { buildProviderChoices, buildQuickSetupFlow, createPersistedSetupOutput, installRuntimeConfigFromStoredSetup, loadSetupOutput, renderInstallSummary, renderSetupSummary, resolveDefaultRuntimeConfigPath, resolveDefaultSetupPath, saveSetupOutput, saveSetupAndRuntimeConfig, serializeSetupOutput, writeRuntimeConfig } from "@longtable/setup";
|
|
14
14
|
import { buildCodexSkillSpecs, buildCodexThinWrappedPrompt, installCodexSkills, listInstalledCodexSkills, renderQuestionRecordPrompt, removeCodexSkills, resolveCodexSkillsDir, runCodexThinWrapper } from "@longtable/provider-codex";
|
|
15
15
|
import { buildClaudeSkillSpecs, installClaudeSkills, listInstalledClaudeSkills, renderQuestionRecordInput, removeClaudeSkills, resolveClaudeSkillsDir } from "@longtable/provider-claude";
|
|
@@ -49,8 +49,35 @@ const require = createRequire(import.meta.url);
|
|
|
49
49
|
const LONGTABLE_PACKAGE_VERSION = String(require("../package.json").version ?? "0.0.0");
|
|
50
50
|
const LONGTABLE_MCP_SERVER_NAME = "longtable-state";
|
|
51
51
|
const LONGTABLE_MCP_PACKAGE_VERSION = LONGTABLE_PACKAGE_VERSION;
|
|
52
|
+
const LONGTABLE_MCP_PACKAGE_SPEC = `@longtable/mcp@${LONGTABLE_MCP_PACKAGE_VERSION}`;
|
|
52
53
|
const LONGTABLE_MCP_MARKER_START = "# LongTable state MCP START";
|
|
53
54
|
const LONGTABLE_MCP_MARKER_END = "# LongTable state MCP END";
|
|
55
|
+
const LONGTABLE_MCP_MANAGED_TOOLS = [
|
|
56
|
+
"read_project",
|
|
57
|
+
"read_session",
|
|
58
|
+
"inspect_workspace",
|
|
59
|
+
"create_workspace",
|
|
60
|
+
"begin_interview",
|
|
61
|
+
"append_interview_turn",
|
|
62
|
+
"summarize_interview",
|
|
63
|
+
"summarize_research_specification",
|
|
64
|
+
"read_research_specification",
|
|
65
|
+
"cancel_interview",
|
|
66
|
+
"confirm_first_research_shape",
|
|
67
|
+
"confirm_research_specification",
|
|
68
|
+
"pending_questions",
|
|
69
|
+
"evaluate_checkpoint",
|
|
70
|
+
"create_question",
|
|
71
|
+
"elicit_question",
|
|
72
|
+
"render_question",
|
|
73
|
+
"append_decision",
|
|
74
|
+
"regenerate_current"
|
|
75
|
+
];
|
|
76
|
+
const LONGTABLE_MCP_RESEARCH_SPECIFICATION_TOOLS = [
|
|
77
|
+
"summarize_research_specification",
|
|
78
|
+
"read_research_specification",
|
|
79
|
+
"confirm_research_specification"
|
|
80
|
+
];
|
|
54
81
|
function style(text, prefix) {
|
|
55
82
|
return `${prefix}${text}${ANSI.reset}`;
|
|
56
83
|
}
|
|
@@ -84,7 +111,7 @@ function renderInterviewLaunchSteps(provider) {
|
|
|
84
111
|
`2. run \`${command}\``,
|
|
85
112
|
"3. invoke `$longtable-interview`",
|
|
86
113
|
"",
|
|
87
|
-
"The interview will create or resume `.longtable/`,
|
|
114
|
+
"The interview will create or resume `.longtable/`, may store a short First Research Shape handle, and uses option UI for the final Research Specification confirmation."
|
|
88
115
|
]);
|
|
89
116
|
}
|
|
90
117
|
function renderProgressBar(current, total) {
|
|
@@ -109,10 +136,11 @@ function usage() {
|
|
|
109
136
|
" longtable show [--json] [--path <file>]",
|
|
110
137
|
" longtable install [--json] [--path <file>] [--runtime-path <file>]",
|
|
111
138
|
" longtable mcp install [--provider codex|claude|all] [--write] [--checkpoint-ui off|interactive|strong] [--json] [--codex-config <path>] [--claude-settings <path>] [--package <spec>]",
|
|
139
|
+
" longtable access setup [--doi <doi>] [--json]",
|
|
140
|
+
" longtable access status [--json]",
|
|
141
|
+
" longtable access doctor [--doi <doi>] [--publisher auto|elsevier|springer_nature|wiley|taylor_francis|all] [--json]",
|
|
142
|
+
" longtable access probe --doi <doi> [--publisher auto|elsevier|springer_nature|wiley|taylor_francis] [--json]",
|
|
112
143
|
" longtable search --query <text> [--intent literature|theory|measurement|citation|metadata|venue] [--field <text>] [--source all|crossref,arxiv,openalex,semantic_scholar,pubmed,eric,doaj,unpaywall] [--must <term[,term]>] [--exclude <term[,term]>] [--limit <n>] [--allow-partial] [--publisher-access] [--record] [--cwd <path>] [--json]",
|
|
113
|
-
" longtable search setup [--doi <doi>] [--json]",
|
|
114
|
-
" longtable search doctor [--doi <doi>] [--publisher auto|elsevier|springer_nature|wiley|taylor_francis|all] [--json]",
|
|
115
|
-
" longtable search probe --doi <doi> [--publisher auto|elsevier|springer_nature|wiley|taylor_francis] [--json]",
|
|
116
144
|
" longtable sentinel --prompt <text> [--cwd <path>] [--json] [--record]",
|
|
117
145
|
" longtable team --prompt <text> [--role <role[,role]>] [--debate] [--rounds 3|5] [--cwd <path>] [--json]",
|
|
118
146
|
" longtable ask [--prompt <text>] [--print] [--json] [--setup <path>] [--cwd <path>]",
|
|
@@ -154,7 +182,7 @@ function parseArgs(argv) {
|
|
|
154
182
|
const values = {};
|
|
155
183
|
let subcommand = maybeSubcommand;
|
|
156
184
|
const modeCommand = command && VALID_MODES.has(command);
|
|
157
|
-
const directCommand = command && ["init", "setup", "start", "resume", "doctor", "status", "audit", "roles", "show", "install", "mcp", "codex", "claude", "ask", "clarify", "question", "clear-question", "prune-questions", "panel", "decide", "sentinel", "team", "search"].includes(command);
|
|
185
|
+
const directCommand = command && ["init", "setup", "start", "resume", "doctor", "status", "audit", "roles", "show", "install", "mcp", "codex", "claude", "ask", "clarify", "question", "clear-question", "prune-questions", "panel", "decide", "sentinel", "team", "access", "search"].includes(command);
|
|
158
186
|
let startIndex = 1;
|
|
159
187
|
if (modeCommand) {
|
|
160
188
|
subcommand = undefined;
|
|
@@ -163,7 +191,7 @@ function parseArgs(argv) {
|
|
|
163
191
|
else if (command === "codex" || command === "claude" || command === "mcp") {
|
|
164
192
|
startIndex = 2;
|
|
165
193
|
}
|
|
166
|
-
else if (command === "search" && maybeSubcommand && !maybeSubcommand.startsWith("--")) {
|
|
194
|
+
else if ((command === "access" || command === "search") && maybeSubcommand && !maybeSubcommand.startsWith("--")) {
|
|
167
195
|
subcommand = maybeSubcommand;
|
|
168
196
|
startIndex = 2;
|
|
169
197
|
}
|
|
@@ -1130,7 +1158,7 @@ function resolveMcpProviders(value) {
|
|
|
1130
1158
|
function resolveMcpPackageSpec(args) {
|
|
1131
1159
|
return typeof args.package === "string" && args.package.trim()
|
|
1132
1160
|
? args.package.trim()
|
|
1133
|
-
:
|
|
1161
|
+
: LONGTABLE_MCP_PACKAGE_SPEC;
|
|
1134
1162
|
}
|
|
1135
1163
|
function resolveCodexMcpConfigPath(args) {
|
|
1136
1164
|
return resolve(normalizeUserPath(typeof args["codex-config"] === "string" && args["codex-config"].trim()
|
|
@@ -1161,6 +1189,12 @@ function renderCodexMcpBlock(serverName, command, mcpArgs) {
|
|
|
1161
1189
|
`[mcp_servers.${serverName}]`,
|
|
1162
1190
|
`command = ${escapeTomlString(command)}`,
|
|
1163
1191
|
`args = [${mcpArgs.map((arg) => escapeTomlString(arg)).join(", ")}]`,
|
|
1192
|
+
"",
|
|
1193
|
+
...LONGTABLE_MCP_MANAGED_TOOLS.flatMap((tool) => [
|
|
1194
|
+
`[mcp_servers.${serverName}.tools.${tool}]`,
|
|
1195
|
+
"approval_mode = \"approve\"",
|
|
1196
|
+
""
|
|
1197
|
+
]),
|
|
1164
1198
|
LONGTABLE_MCP_MARKER_END
|
|
1165
1199
|
].join("\n");
|
|
1166
1200
|
}
|
|
@@ -1184,10 +1218,55 @@ function codexMcpElicitationsAllowed(config) {
|
|
|
1184
1218
|
function codexLongTableMcpConfigured(config) {
|
|
1185
1219
|
return new RegExp(`\\[mcp_servers\\.${LONGTABLE_MCP_SERVER_NAME.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\]`).test(config);
|
|
1186
1220
|
}
|
|
1221
|
+
function codexLongTableMcpPackageSpec(config) {
|
|
1222
|
+
const serverName = LONGTABLE_MCP_SERVER_NAME.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1223
|
+
const match = new RegExp(`\\[mcp_servers\\.${serverName}\\][\\s\\S]*?(?=\\n\\[|$)`).exec(config);
|
|
1224
|
+
if (!match) {
|
|
1225
|
+
return undefined;
|
|
1226
|
+
}
|
|
1227
|
+
const packageMatch = /@longtable\/mcp@[A-Za-z0-9._~+:-]+/.exec(match[0]);
|
|
1228
|
+
return packageMatch?.[0];
|
|
1229
|
+
}
|
|
1230
|
+
function codexLongTableMcpToolConfigured(config, tool) {
|
|
1231
|
+
const serverName = LONGTABLE_MCP_SERVER_NAME.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1232
|
+
const escapedTool = tool.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1233
|
+
return new RegExp(`\\[mcp_servers\\.${serverName}\\.tools\\.${escapedTool}\\]`).test(config);
|
|
1234
|
+
}
|
|
1235
|
+
function missingCodexLongTableMcpTools(config) {
|
|
1236
|
+
if (!codexLongTableMcpConfigured(config)) {
|
|
1237
|
+
return [...LONGTABLE_MCP_MANAGED_TOOLS];
|
|
1238
|
+
}
|
|
1239
|
+
return LONGTABLE_MCP_MANAGED_TOOLS.filter((tool) => !codexLongTableMcpToolConfigured(config, tool));
|
|
1240
|
+
}
|
|
1241
|
+
function preserveNonLongTableSectionsFromMarkedBlock(block, serverName) {
|
|
1242
|
+
const body = block
|
|
1243
|
+
.replace(LONGTABLE_MCP_MARKER_START, "")
|
|
1244
|
+
.replace(LONGTABLE_MCP_MARKER_END, "")
|
|
1245
|
+
.trim();
|
|
1246
|
+
if (!body) {
|
|
1247
|
+
return "";
|
|
1248
|
+
}
|
|
1249
|
+
const sections = body.split(/(?=^\[[^\]]+\])/m);
|
|
1250
|
+
const serverHeader = `[mcp_servers.${serverName}]`;
|
|
1251
|
+
const toolPrefix = `[mcp_servers.${serverName}.tools.`;
|
|
1252
|
+
return sections
|
|
1253
|
+
.map((section) => section.trim())
|
|
1254
|
+
.filter(Boolean)
|
|
1255
|
+
.filter((section) => {
|
|
1256
|
+
const header = section.split(/\r?\n/, 1)[0]?.trim() ?? "";
|
|
1257
|
+
return header !== serverHeader && !header.startsWith(toolPrefix);
|
|
1258
|
+
})
|
|
1259
|
+
.join("\n\n");
|
|
1260
|
+
}
|
|
1187
1261
|
function replaceMarkedCodexMcpBlock(existing, block, serverName) {
|
|
1188
|
-
const markerPattern = new RegExp(`${LONGTABLE_MCP_MARKER_START.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[\\s\\S]*?${LONGTABLE_MCP_MARKER_END.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\n
|
|
1189
|
-
const serverPattern = new RegExp(`\\n?\\[mcp_servers\\.${serverName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\][\\s\\S]*?(?=\\n\\[|$)
|
|
1190
|
-
const
|
|
1262
|
+
const markerPattern = new RegExp(`${LONGTABLE_MCP_MARKER_START.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[\\s\\S]*?${LONGTABLE_MCP_MARKER_END.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\n?`);
|
|
1263
|
+
const serverPattern = new RegExp(`\\n?\\[mcp_servers\\.${serverName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\][\\s\\S]*?(?=\\n\\[|$)`);
|
|
1264
|
+
const toolPattern = new RegExp(`\\n?\\[mcp_servers\\.${serverName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\.tools\\.[^\\]]+\\][\\s\\S]*?(?=\\n\\[|$)`, "g");
|
|
1265
|
+
const withoutMarked = existing.replace(markerPattern, (matched) => {
|
|
1266
|
+
const preserved = preserveNonLongTableSectionsFromMarkedBlock(matched, serverName);
|
|
1267
|
+
return preserved ? `${preserved}\n\n` : "";
|
|
1268
|
+
});
|
|
1269
|
+
const trimmed = withoutMarked.replace(toolPattern, "").replace(serverPattern, "").trimEnd();
|
|
1191
1270
|
return trimmed ? `${trimmed}\n\n${block}\n` : `${block}\n`;
|
|
1192
1271
|
}
|
|
1193
1272
|
async function writeCodexMcpConfig(path, block, serverName, options = {}) {
|
|
@@ -1466,6 +1545,8 @@ async function collectDoctorStatus(args) {
|
|
|
1466
1545
|
const codexMcpConfig = existsSync(codexMcpConfigPath)
|
|
1467
1546
|
? await readFile(codexMcpConfigPath, "utf8")
|
|
1468
1547
|
: "";
|
|
1548
|
+
const codexMcpPackageSpec = codexLongTableMcpPackageSpec(codexMcpConfig);
|
|
1549
|
+
const missingMcpTools = missingCodexLongTableMcpTools(codexMcpConfig);
|
|
1469
1550
|
const codexHooksPath = resolveCodexHooksPath(args);
|
|
1470
1551
|
const codexHooksContent = existsSync(codexHooksPath)
|
|
1471
1552
|
? await readFile(codexHooksPath, "utf8")
|
|
@@ -1501,6 +1582,10 @@ async function collectDoctorStatus(args) {
|
|
|
1501
1582
|
mcpConfigPath: codexMcpConfigPath,
|
|
1502
1583
|
mcpConfigExists: existsSync(codexMcpConfigPath),
|
|
1503
1584
|
longtableMcpConfigured: codexLongTableMcpConfigured(codexMcpConfig),
|
|
1585
|
+
...(codexMcpPackageSpec ? { mcpPackageSpec: codexMcpPackageSpec } : {}),
|
|
1586
|
+
expectedMcpPackageSpec: LONGTABLE_MCP_PACKAGE_SPEC,
|
|
1587
|
+
missingMcpTools,
|
|
1588
|
+
missingResearchSpecificationMcpTools: missingMcpTools.filter((tool) => LONGTABLE_MCP_RESEARCH_SPECIFICATION_TOOLS.includes(tool)),
|
|
1504
1589
|
mcpElicitationsAllowed: codexMcpElicitationsAllowed(codexMcpConfig),
|
|
1505
1590
|
hooksPath: codexHooksPath,
|
|
1506
1591
|
hooksExists: existsSync(codexHooksPath),
|
|
@@ -1546,6 +1631,11 @@ function renderDoctorStatus(status) {
|
|
|
1546
1631
|
: []),
|
|
1547
1632
|
`- MCP config: ${status.providers.codex.mcpConfigExists ? "present" : "missing"} (${status.providers.codex.mcpConfigPath})`,
|
|
1548
1633
|
`- LongTable MCP: ${status.providers.codex.longtableMcpConfigured ? "configured" : "missing"}`,
|
|
1634
|
+
`- MCP package: ${status.providers.codex.mcpPackageSpec ?? "unknown"}${status.providers.codex.mcpPackageSpec === status.providers.codex.expectedMcpPackageSpec ? "" : ` (expected ${status.providers.codex.expectedMcpPackageSpec})`}`,
|
|
1635
|
+
`- MCP managed tools: ${LONGTABLE_MCP_MANAGED_TOOLS.length - status.providers.codex.missingMcpTools.length}/${LONGTABLE_MCP_MANAGED_TOOLS.length} configured`,
|
|
1636
|
+
...(status.providers.codex.missingResearchSpecificationMcpTools.length > 0
|
|
1637
|
+
? [`- Research Specification MCP tools: missing ${status.providers.codex.missingResearchSpecificationMcpTools.join(", ")}`]
|
|
1638
|
+
: ["- Research Specification MCP tools: complete"]),
|
|
1549
1639
|
`- MCP elicitation approval: ${status.providers.codex.mcpElicitationsAllowed ? "allowed" : "not allowed"}`,
|
|
1550
1640
|
`- Codex hooks file: ${status.providers.codex.hooksExists ? "present" : "missing"} (${status.providers.codex.hooksPath})`,
|
|
1551
1641
|
`- codex_hooks feature: ${status.providers.codex.codexHooksEnabled ? "enabled" : "missing"}`,
|
|
@@ -1594,6 +1684,9 @@ function renderDoctorStatus(status) {
|
|
|
1594
1684
|
const canFix = status.providers.codex.missingSkills.length > 0 ||
|
|
1595
1685
|
status.providers.claude.missingSkills.length > 0 ||
|
|
1596
1686
|
status.providers.codex.legacyPromptFilesInstalled.length > 0 ||
|
|
1687
|
+
!status.providers.codex.longtableMcpConfigured ||
|
|
1688
|
+
status.providers.codex.mcpPackageSpec !== status.providers.codex.expectedMcpPackageSpec ||
|
|
1689
|
+
status.providers.codex.missingMcpTools.length > 0 ||
|
|
1597
1690
|
!status.providers.codex.codexHooksEnabled ||
|
|
1598
1691
|
status.providers.codex.missingManagedHookEvents.length > 0 ||
|
|
1599
1692
|
(status.setupExists &&
|
|
@@ -1604,6 +1697,11 @@ function renderDoctorStatus(status) {
|
|
|
1604
1697
|
if (!status.providers.codex.codexHooksEnabled || status.providers.codex.missingManagedHookEvents.length > 0) {
|
|
1605
1698
|
nextActions.push("longtable codex install-hooks");
|
|
1606
1699
|
}
|
|
1700
|
+
if (!status.providers.codex.longtableMcpConfigured ||
|
|
1701
|
+
status.providers.codex.mcpPackageSpec !== status.providers.codex.expectedMcpPackageSpec ||
|
|
1702
|
+
status.providers.codex.missingMcpTools.length > 0) {
|
|
1703
|
+
nextActions.push("longtable mcp install --provider codex --write");
|
|
1704
|
+
}
|
|
1607
1705
|
if (!status.setupExists) {
|
|
1608
1706
|
nextActions.push("longtable setup --provider codex");
|
|
1609
1707
|
}
|
|
@@ -1643,7 +1741,7 @@ function renderRepairSummary(repair) {
|
|
|
1643
1741
|
}
|
|
1644
1742
|
}
|
|
1645
1743
|
if (repair.writtenRuntimeConfigs.length > 0) {
|
|
1646
|
-
lines.push("- wrote
|
|
1744
|
+
lines.push("- wrote configs:");
|
|
1647
1745
|
for (const target of repair.writtenRuntimeConfigs) {
|
|
1648
1746
|
lines.push(` - ${target.provider}: ${target.path}`);
|
|
1649
1747
|
}
|
|
@@ -1699,6 +1797,21 @@ async function repairDoctorStatus(args, status) {
|
|
|
1699
1797
|
writtenRuntimeConfigs: [],
|
|
1700
1798
|
skipped: []
|
|
1701
1799
|
};
|
|
1800
|
+
const mcpRepairNeeded = !status.providers.codex.longtableMcpConfigured ||
|
|
1801
|
+
status.providers.codex.mcpPackageSpec !== status.providers.codex.expectedMcpPackageSpec ||
|
|
1802
|
+
status.providers.codex.missingMcpTools.length > 0;
|
|
1803
|
+
if (mcpRepairNeeded) {
|
|
1804
|
+
const mcpConfigPath = resolveDoctorCodexMcpConfigPath(args);
|
|
1805
|
+
const block = renderCodexMcpBlock(LONGTABLE_MCP_SERVER_NAME, "npx", ["-y", LONGTABLE_MCP_PACKAGE_SPEC]);
|
|
1806
|
+
await writeCodexMcpConfig(mcpConfigPath, block, LONGTABLE_MCP_SERVER_NAME, {
|
|
1807
|
+
enableElicitations: status.providers.codex.mcpElicitationsAllowed
|
|
1808
|
+
});
|
|
1809
|
+
repair.writtenRuntimeConfigs.push({
|
|
1810
|
+
provider: "codex",
|
|
1811
|
+
path: mcpConfigPath,
|
|
1812
|
+
format: "toml"
|
|
1813
|
+
});
|
|
1814
|
+
}
|
|
1702
1815
|
if (status.providers.codex.missingSkills.length > 0) {
|
|
1703
1816
|
repair.installedCodexSkills = (await installCodexSkills(roles, codexDir, skillSurface)).map((skill) => skill.name);
|
|
1704
1817
|
}
|
|
@@ -1789,6 +1902,11 @@ const QUESTION_AUDIT_FIXTURES = [
|
|
|
1789
1902
|
prompt: "Protected decision closure pressure: measurement. User prompt: Implement the plan.",
|
|
1790
1903
|
expectedKinds: ["research_commitment"]
|
|
1791
1904
|
},
|
|
1905
|
+
{
|
|
1906
|
+
id: "scholarly_access_policy",
|
|
1907
|
+
prompt: "메타분석 논문들의 PDF와 full text를 수집해서 원문 기반으로 코딩해줘.",
|
|
1908
|
+
expectedKinds: ["evidence_risk"]
|
|
1909
|
+
},
|
|
1792
1910
|
{
|
|
1793
1911
|
id: "low_stakes_copyedit",
|
|
1794
1912
|
prompt: "문장 끝 공백만 정리해줘.",
|
|
@@ -2347,6 +2465,86 @@ async function recordEvidenceRun(run, workingDirectory) {
|
|
|
2347
2465
|
await syncCurrentWorkspaceView(context);
|
|
2348
2466
|
return evidencePath;
|
|
2349
2467
|
}
|
|
2468
|
+
function nowIso() {
|
|
2469
|
+
return new Date().toISOString();
|
|
2470
|
+
}
|
|
2471
|
+
function uniqueAccessRoutes(routes) {
|
|
2472
|
+
return [...new Set(routes)];
|
|
2473
|
+
}
|
|
2474
|
+
function accessReadinessPath(home = homedir()) {
|
|
2475
|
+
return join(home, ".longtable", "access-readiness.json");
|
|
2476
|
+
}
|
|
2477
|
+
function readAccessReadinessProfile() {
|
|
2478
|
+
const path = accessReadinessPath();
|
|
2479
|
+
if (!existsSync(path)) {
|
|
2480
|
+
return undefined;
|
|
2481
|
+
}
|
|
2482
|
+
try {
|
|
2483
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
2484
|
+
}
|
|
2485
|
+
catch {
|
|
2486
|
+
return undefined;
|
|
2487
|
+
}
|
|
2488
|
+
}
|
|
2489
|
+
function hasPublisherCredentialSignal(records) {
|
|
2490
|
+
return records.some((record) => record.presentEnv.length > 0 || record.credentialStatus !== "missing");
|
|
2491
|
+
}
|
|
2492
|
+
function readinessPublisherRecord(record) {
|
|
2493
|
+
const safeRecord = { ...record };
|
|
2494
|
+
delete safeRecord.evidenceSnippet;
|
|
2495
|
+
return safeRecord;
|
|
2496
|
+
}
|
|
2497
|
+
function inferAccessRoutes(records) {
|
|
2498
|
+
const routes = ["metadata"];
|
|
2499
|
+
if (hasPublisherCredentialSignal(records)) {
|
|
2500
|
+
routes.push("publisher_tdm");
|
|
2501
|
+
}
|
|
2502
|
+
return routes;
|
|
2503
|
+
}
|
|
2504
|
+
function buildAccessReadinessProfile(options) {
|
|
2505
|
+
const publisherRecords = options.publisherRecords ?? summarizeConfiguredPublisherAccess(env);
|
|
2506
|
+
const routes = uniqueAccessRoutes(options.routes?.length ? options.routes : inferAccessRoutes(publisherRecords));
|
|
2507
|
+
const disabled = options.readiness === "disabled";
|
|
2508
|
+
const institutionalMode = options.institutionalAccessMode;
|
|
2509
|
+
const institutionalAccess = routes.includes("institutional")
|
|
2510
|
+
? {
|
|
2511
|
+
available: true,
|
|
2512
|
+
mode: institutionalMode ?? "unknown",
|
|
2513
|
+
verified: false,
|
|
2514
|
+
note: "LongTable records institutional access readiness only. The researcher must complete VPN/proxy/library login directly."
|
|
2515
|
+
}
|
|
2516
|
+
: undefined;
|
|
2517
|
+
return {
|
|
2518
|
+
version: 1,
|
|
2519
|
+
updatedAt: nowIso(),
|
|
2520
|
+
readiness: options.readiness,
|
|
2521
|
+
metadataSources: [...SEARCH_SOURCES],
|
|
2522
|
+
routes: disabled ? [] : routes,
|
|
2523
|
+
...(institutionalAccess ? { institutionalAccess } : {}),
|
|
2524
|
+
publisherTdm: disabled
|
|
2525
|
+
? "deferred"
|
|
2526
|
+
: routes.includes("publisher_tdm")
|
|
2527
|
+
? hasPublisherCredentialSignal(publisherRecords) ? "configured" : "unknown"
|
|
2528
|
+
: "not_configured",
|
|
2529
|
+
oaOnly: !disabled && routes.includes("oa_full_text") && !routes.includes("institutional") && !routes.includes("publisher_tdm"),
|
|
2530
|
+
manualPdfAllowed: !disabled && routes.includes("manual_pdf"),
|
|
2531
|
+
storesSecrets: false,
|
|
2532
|
+
requiresCheckpointBeforeSearch: options.readiness === "deferred",
|
|
2533
|
+
requiresCheckpointBeforeFullText: options.readiness !== "disabled",
|
|
2534
|
+
...(disabled ? {} : {
|
|
2535
|
+
publisherAccess: {
|
|
2536
|
+
contactEmailPresent: Boolean(env.LONGTABLE_CONTACT_EMAIL?.trim()),
|
|
2537
|
+
records: publisherRecords.map(readinessPublisherRecord)
|
|
2538
|
+
}
|
|
2539
|
+
})
|
|
2540
|
+
};
|
|
2541
|
+
}
|
|
2542
|
+
async function saveAccessReadinessProfile(profile) {
|
|
2543
|
+
const profilePath = accessReadinessPath();
|
|
2544
|
+
await mkdir(dirname(profilePath), { recursive: true });
|
|
2545
|
+
await writeJsonFile(profilePath, profile);
|
|
2546
|
+
return profilePath;
|
|
2547
|
+
}
|
|
2350
2548
|
function renderPublisherAccessRecord(record) {
|
|
2351
2549
|
const envSummary = record.missingEnv.length > 0
|
|
2352
2550
|
? `missing ${record.missingEnv.join(", ")}`
|
|
@@ -2381,12 +2579,6 @@ function renderPublisherAccessRecords(title, records, capabilityPath) {
|
|
|
2381
2579
|
}
|
|
2382
2580
|
return lines.join("\n");
|
|
2383
2581
|
}
|
|
2384
|
-
async function saveSearchCapabilityRecords(records) {
|
|
2385
|
-
const snapshotPath = searchCapabilitySnapshotPath();
|
|
2386
|
-
await mkdir(dirname(snapshotPath), { recursive: true });
|
|
2387
|
-
await writeJsonFile(snapshotPath, buildSearchCapabilitySnapshot(records, env));
|
|
2388
|
-
return snapshotPath;
|
|
2389
|
-
}
|
|
2390
2582
|
async function probeAllPublishers(doi) {
|
|
2391
2583
|
const records = [];
|
|
2392
2584
|
for (const publisher of publisherConfigs()) {
|
|
@@ -2398,9 +2590,48 @@ async function probeAllPublishers(doi) {
|
|
|
2398
2590
|
}
|
|
2399
2591
|
return records;
|
|
2400
2592
|
}
|
|
2401
|
-
|
|
2593
|
+
function renderAccessReadinessProfile(profile, profilePath = accessReadinessPath()) {
|
|
2594
|
+
const lines = [
|
|
2595
|
+
"LongTable Scholarly Access Readiness",
|
|
2596
|
+
`- profile: ${profilePath}`,
|
|
2597
|
+
`- readiness: ${profile.readiness}`,
|
|
2598
|
+
`- routes: ${profile.routes.length > 0 ? profile.routes.join(", ") : "none"}`,
|
|
2599
|
+
`- metadata sources: ${profile.metadataSources.join(", ")}`,
|
|
2600
|
+
`- OA-only: ${profile.oaOnly ? "yes" : "no"}`,
|
|
2601
|
+
`- manual PDF allowed: ${profile.manualPdfAllowed ? "yes" : "no"}`,
|
|
2602
|
+
`- publisher/TDM: ${profile.publisherTdm}`,
|
|
2603
|
+
`- stores secrets: ${profile.storesSecrets ? "yes" : "no"}`,
|
|
2604
|
+
`- checkpoint before search: ${profile.requiresCheckpointBeforeSearch ? "yes" : "no"}`,
|
|
2605
|
+
`- checkpoint before full text: ${profile.requiresCheckpointBeforeFullText ? "yes" : "no"}`
|
|
2606
|
+
];
|
|
2607
|
+
if (profile.institutionalAccess) {
|
|
2608
|
+
lines.push(`- institutional access: ${profile.institutionalAccess.mode}; verified: no`);
|
|
2609
|
+
lines.push(` note: ${profile.institutionalAccess.note}`);
|
|
2610
|
+
}
|
|
2611
|
+
if (profile.publisherAccess) {
|
|
2612
|
+
lines.push(`- contact email: ${profile.publisherAccess.contactEmailPresent ? "present" : "missing"}`);
|
|
2613
|
+
lines.push(`- publisher adapters: ${profile.publisherAccess.records.length}`);
|
|
2614
|
+
}
|
|
2615
|
+
return lines.join("\n");
|
|
2616
|
+
}
|
|
2617
|
+
function renderAccessDoctor(profile, records, profilePath) {
|
|
2618
|
+
const metadataCapabilities = assessSearchSourceCapabilities([...SEARCH_SOURCES], env);
|
|
2619
|
+
const lines = [
|
|
2620
|
+
"LongTable Scholarly Access Doctor",
|
|
2621
|
+
`- readiness profile: ${profile ? "present" : "missing"} (${profilePath})`,
|
|
2622
|
+
`- metadata sources: ${metadataCapabilities.map((capability) => `${capability.source}:${capability.enabled ? "available" : "needs_config"}`).join(", ")}`,
|
|
2623
|
+
"- institutional access: LongTable cannot verify VPN/proxy/SSO until the researcher logs in.",
|
|
2624
|
+
"- secrets: LongTable stores env var names and capability status only, never credential values.",
|
|
2625
|
+
renderPublisherAccessRecords("Publisher API/TDM adapters", records)
|
|
2626
|
+
];
|
|
2627
|
+
if (!profile) {
|
|
2628
|
+
lines.push("- next: run `longtable access setup` before PDF collection or full-text extraction.");
|
|
2629
|
+
}
|
|
2630
|
+
return lines.join("\n");
|
|
2631
|
+
}
|
|
2632
|
+
async function runAccessProbe(args) {
|
|
2402
2633
|
if (typeof args.doi !== "string" || !args.doi.trim()) {
|
|
2403
|
-
throw new Error("`longtable
|
|
2634
|
+
throw new Error("`longtable access probe` requires --doi <doi>.");
|
|
2404
2635
|
}
|
|
2405
2636
|
const publisher = parsePublisherTarget(args.publisher);
|
|
2406
2637
|
const record = await probePublisherAccess({
|
|
@@ -2416,7 +2647,7 @@ async function runSearchProbe(args) {
|
|
|
2416
2647
|
}
|
|
2417
2648
|
return [record];
|
|
2418
2649
|
}
|
|
2419
|
-
async function
|
|
2650
|
+
async function collectAccessDoctorRecords(args) {
|
|
2420
2651
|
let records;
|
|
2421
2652
|
if (typeof args.doi === "string" && args.doi.trim()) {
|
|
2422
2653
|
if (args.publisher === "all") {
|
|
@@ -2434,104 +2665,148 @@ async function runSearchDoctor(args) {
|
|
|
2434
2665
|
else {
|
|
2435
2666
|
records = summarizeConfiguredPublisherAccess(env);
|
|
2436
2667
|
}
|
|
2437
|
-
|
|
2438
|
-
|
|
2668
|
+
return records;
|
|
2669
|
+
}
|
|
2670
|
+
async function runAccessDoctor(args) {
|
|
2671
|
+
const records = await collectAccessDoctorRecords(args);
|
|
2672
|
+
const profilePath = accessReadinessPath();
|
|
2673
|
+
const profile = readAccessReadinessProfile();
|
|
2439
2674
|
if (args.json === true) {
|
|
2440
2675
|
console.log(JSON.stringify({
|
|
2441
|
-
|
|
2442
|
-
|
|
2676
|
+
readinessFile: profilePath,
|
|
2677
|
+
readinessFileExists: Boolean(profile),
|
|
2678
|
+
readiness: profile,
|
|
2679
|
+
metadataSources: assessSearchSourceCapabilities([...SEARCH_SOURCES], env),
|
|
2443
2680
|
records
|
|
2444
2681
|
}, null, 2));
|
|
2445
2682
|
}
|
|
2446
2683
|
else {
|
|
2447
|
-
console.log(
|
|
2448
|
-
if (!snapshotExists) {
|
|
2449
|
-
console.log("- saved capabilities: none yet; run `longtable search setup` to record non-secret capability status.");
|
|
2450
|
-
}
|
|
2684
|
+
console.log(renderAccessDoctor(profile, records, profilePath));
|
|
2451
2685
|
}
|
|
2452
2686
|
return records;
|
|
2453
2687
|
}
|
|
2454
|
-
async function
|
|
2455
|
-
const
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
const answer = (await rl.question(prompt)).trim();
|
|
2459
|
-
if (!answer && defaultDoi) {
|
|
2460
|
-
return defaultDoi;
|
|
2461
|
-
}
|
|
2462
|
-
if (!answer || /^skip$/i.test(answer)) {
|
|
2463
|
-
return undefined;
|
|
2688
|
+
async function publisherRecordsForAccessSetup(args, routes) {
|
|
2689
|
+
const defaultDoi = typeof args.doi === "string" ? args.doi : undefined;
|
|
2690
|
+
if (!routes.includes("publisher_tdm")) {
|
|
2691
|
+
return summarizeConfiguredPublisherAccess(env);
|
|
2464
2692
|
}
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
async function runInteractiveSearchSetup(defaultDoi) {
|
|
2468
|
-
const rl = createInterface({ input, output });
|
|
2469
|
-
const records = [];
|
|
2470
|
-
try {
|
|
2471
|
-
console.log("LongTable publisher access setup");
|
|
2472
|
-
console.log("LongTable does not store API keys or TDM tokens. It reads environment variables and records only non-secret capability results.");
|
|
2473
|
-
console.log("");
|
|
2474
|
-
for (const publisher of publisherConfigs()) {
|
|
2475
|
-
console.log(`${publisher.label}`);
|
|
2476
|
-
console.log(` required env: ${publisher.requiredEnv.join(", ")}`);
|
|
2477
|
-
if (publisher.optionalEnv.length > 0) {
|
|
2478
|
-
console.log(` optional env: ${publisher.optionalEnv.join(", ")}`);
|
|
2479
|
-
}
|
|
2480
|
-
console.log(` ${publisher.setupHint}`);
|
|
2481
|
-
const doi = await promptPublisherDoi(rl, publisher.label, defaultDoi);
|
|
2482
|
-
if (doi) {
|
|
2483
|
-
records.push(await probePublisherAccess({
|
|
2484
|
-
doi,
|
|
2485
|
-
publisher: publisher.publisher,
|
|
2486
|
-
env
|
|
2487
|
-
}));
|
|
2488
|
-
}
|
|
2489
|
-
else {
|
|
2490
|
-
const summary = summarizeConfiguredPublisherAccess(env)
|
|
2491
|
-
.find((record) => record.publisher === publisher.publisher);
|
|
2492
|
-
if (summary) {
|
|
2493
|
-
records.push(summary);
|
|
2494
|
-
}
|
|
2495
|
-
}
|
|
2496
|
-
console.log(renderPublisherAccessRecord(records[records.length - 1]));
|
|
2497
|
-
console.log("");
|
|
2498
|
-
}
|
|
2693
|
+
if (defaultDoi) {
|
|
2694
|
+
return probeAllPublishers(defaultDoi);
|
|
2499
2695
|
}
|
|
2500
|
-
|
|
2501
|
-
|
|
2696
|
+
if (input.isTTY && output.isTTY && args.json !== true) {
|
|
2697
|
+
const doi = await promptText("Optional DOI for publisher/TDM probing. Leave blank to record env-var readiness only.", false);
|
|
2698
|
+
return doi
|
|
2699
|
+
? await probeAllPublishers(doi)
|
|
2700
|
+
: summarizeConfiguredPublisherAccess(env);
|
|
2502
2701
|
}
|
|
2503
|
-
return
|
|
2702
|
+
return summarizeConfiguredPublisherAccess(env);
|
|
2504
2703
|
}
|
|
2505
|
-
async function
|
|
2506
|
-
const
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
:
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2704
|
+
async function runInteractiveAccessSetup(args) {
|
|
2705
|
+
const readiness = await promptChoice("Scholarly Access Readiness\n\nWill this machine/account use scholarly search or full-text access?", [
|
|
2706
|
+
{ id: "configured", label: "Configure now", description: "Record access capability without storing secrets." },
|
|
2707
|
+
{ id: "deferred", label: "Ask later", description: "Defer setup and require an access checkpoint before search or extraction." },
|
|
2708
|
+
{ id: "disabled", label: "Do not use", description: "This project will use metadata/manual notes without scholarly full-text access." }
|
|
2709
|
+
]);
|
|
2710
|
+
if (readiness !== "configured") {
|
|
2711
|
+
return buildAccessReadinessProfile({ readiness, routes: [] });
|
|
2712
|
+
}
|
|
2713
|
+
const routeSelections = await promptMultiChoice("Select every scholarly access route that is available or intended. Secrets are never stored.", [
|
|
2714
|
+
{ id: "metadata", label: "Open metadata", description: "Crossref, OpenAlex, Semantic Scholar, PubMed, ERIC, DOAJ, Unpaywall." },
|
|
2715
|
+
{ id: "oa_full_text", label: "OA full text", description: "Use open-access PDF/full-text when legally available." },
|
|
2716
|
+
{ id: "institutional", label: "Institutional access", description: "VPN, library proxy, or browser SSO handled by the researcher." },
|
|
2717
|
+
{ id: "publisher_tdm", label: "Publisher API/TDM", description: "Use configured publisher API/TDM environment variables." },
|
|
2718
|
+
{ id: "manual_pdf", label: "Manual PDFs", description: "Researcher supplies PDFs; LongTable organizes/probes allowed extraction." },
|
|
2719
|
+
{ id: "unknown", label: "Unknown", description: "Keep access uncertain and require a checkpoint before full-text work." }
|
|
2720
|
+
]);
|
|
2721
|
+
const routes = uniqueAccessRoutes(routeSelections.length > 0 ? routeSelections : ["metadata"]);
|
|
2722
|
+
const institutionalAccessMode = routes.includes("institutional")
|
|
2723
|
+
? await promptChoice("How will institutional access be completed? The researcher handles login/MFA directly.", [
|
|
2724
|
+
{ id: "vpn", label: "VPN", description: "Researcher connects through school or institutional VPN." },
|
|
2725
|
+
{ id: "library_proxy", label: "Library proxy", description: "Researcher uses proxy links or library resolver." },
|
|
2726
|
+
{ id: "browser_sso", label: "Browser SSO", description: "Researcher logs into library/publisher SSO in the browser." },
|
|
2727
|
+
{ id: "unknown", label: "Unknown", description: "The route exists but is not yet specified." }
|
|
2728
|
+
])
|
|
2729
|
+
: undefined;
|
|
2730
|
+
const publisherRecords = await publisherRecordsForAccessSetup(args, routes);
|
|
2731
|
+
return buildAccessReadinessProfile({
|
|
2732
|
+
readiness,
|
|
2733
|
+
routes,
|
|
2734
|
+
institutionalAccessMode,
|
|
2735
|
+
publisherRecords
|
|
2736
|
+
});
|
|
2737
|
+
}
|
|
2738
|
+
async function runAccessSetup(args) {
|
|
2739
|
+
const records = typeof args.doi === "string" && args.doi.trim()
|
|
2740
|
+
? await probeAllPublishers(args.doi)
|
|
2741
|
+
: summarizeConfiguredPublisherAccess(env);
|
|
2742
|
+
const profile = input.isTTY && output.isTTY && args.json !== true
|
|
2743
|
+
? await runInteractiveAccessSetup(args)
|
|
2744
|
+
: buildAccessReadinessProfile({
|
|
2745
|
+
readiness: "configured",
|
|
2746
|
+
routes: inferAccessRoutes(records),
|
|
2747
|
+
publisherRecords: records
|
|
2748
|
+
});
|
|
2749
|
+
const profilePath = await saveAccessReadinessProfile(profile);
|
|
2513
2750
|
if (args.json === true) {
|
|
2514
2751
|
console.log(JSON.stringify({
|
|
2515
|
-
|
|
2516
|
-
|
|
2752
|
+
readinessFile: profilePath,
|
|
2753
|
+
profile
|
|
2517
2754
|
}, null, 2));
|
|
2518
2755
|
return;
|
|
2519
2756
|
}
|
|
2520
|
-
console.log(
|
|
2757
|
+
console.log(renderAccessReadinessProfile(profile, profilePath));
|
|
2521
2758
|
}
|
|
2522
|
-
async function
|
|
2523
|
-
|
|
2524
|
-
|
|
2759
|
+
async function runAccessStatus(args) {
|
|
2760
|
+
const profilePath = accessReadinessPath();
|
|
2761
|
+
const profile = readAccessReadinessProfile();
|
|
2762
|
+
if (args.json === true) {
|
|
2763
|
+
console.log(JSON.stringify({
|
|
2764
|
+
readinessFile: profilePath,
|
|
2765
|
+
readinessFileExists: Boolean(profile),
|
|
2766
|
+
readiness: profile
|
|
2767
|
+
}, null, 2));
|
|
2525
2768
|
return;
|
|
2526
2769
|
}
|
|
2527
|
-
if (
|
|
2528
|
-
|
|
2770
|
+
if (!profile) {
|
|
2771
|
+
console.log([
|
|
2772
|
+
"LongTable Scholarly Access Readiness",
|
|
2773
|
+
`- profile: missing (${profilePath})`,
|
|
2774
|
+
"- next: run `longtable access setup` before PDF collection or full-text extraction."
|
|
2775
|
+
].join("\n"));
|
|
2529
2776
|
return;
|
|
2530
2777
|
}
|
|
2778
|
+
console.log(renderAccessReadinessProfile(profile, profilePath));
|
|
2779
|
+
}
|
|
2780
|
+
async function runAccess(subcommand, args) {
|
|
2531
2781
|
if (subcommand === "setup") {
|
|
2532
|
-
await
|
|
2782
|
+
await runAccessSetup(args);
|
|
2783
|
+
return;
|
|
2784
|
+
}
|
|
2785
|
+
if (subcommand === "status") {
|
|
2786
|
+
await runAccessStatus(args);
|
|
2533
2787
|
return;
|
|
2534
2788
|
}
|
|
2789
|
+
if (subcommand === "doctor") {
|
|
2790
|
+
await runAccessDoctor(args);
|
|
2791
|
+
return;
|
|
2792
|
+
}
|
|
2793
|
+
if (subcommand === "probe") {
|
|
2794
|
+
await runAccessProbe(args);
|
|
2795
|
+
return;
|
|
2796
|
+
}
|
|
2797
|
+
if (!subcommand) {
|
|
2798
|
+
await runAccessStatus(args);
|
|
2799
|
+
return;
|
|
2800
|
+
}
|
|
2801
|
+
throw new Error(`Unknown access subcommand: ${subcommand}`);
|
|
2802
|
+
}
|
|
2803
|
+
function movedSearchAccessCommand(subcommand) {
|
|
2804
|
+
return new Error(`\`longtable search ${subcommand}\` has moved. Use \`longtable access ${subcommand}\`.`);
|
|
2805
|
+
}
|
|
2806
|
+
async function runSearch(subcommand, args) {
|
|
2807
|
+
if (subcommand === "setup" || subcommand === "doctor" || subcommand === "status" || subcommand === "probe") {
|
|
2808
|
+
throw movedSearchAccessCommand(subcommand);
|
|
2809
|
+
}
|
|
2535
2810
|
if (subcommand) {
|
|
2536
2811
|
throw new Error(`Unknown search subcommand: ${subcommand}`);
|
|
2537
2812
|
}
|
|
@@ -3524,6 +3799,10 @@ async function main() {
|
|
|
3524
3799
|
await runMcpSubcommand(subcommand, values);
|
|
3525
3800
|
return;
|
|
3526
3801
|
}
|
|
3802
|
+
if (command === "access") {
|
|
3803
|
+
await runAccess(subcommand, values);
|
|
3804
|
+
return;
|
|
3805
|
+
}
|
|
3527
3806
|
if (command === "search") {
|
|
3528
3807
|
await runSearch(subcommand, values);
|
|
3529
3808
|
return;
|
|
@@ -131,6 +131,11 @@ function looksLikeResearchCommitmentPrompt(prompt) {
|
|
|
131
131
|
/\b(change|revise|update|replace|reframe|modify|alter)\b/i.test(prompt) ||
|
|
132
132
|
/바꾸|변경|수정|교체|전환|재설정/.test(prompt));
|
|
133
133
|
}
|
|
134
|
+
function looksLikeAccessSensitiveResearchAction(prompt) {
|
|
135
|
+
const normalized = prompt.trim();
|
|
136
|
+
return /\b(pdf|full[- ]?text|tdm|publisher api|institutional access|library login|vpn|proxy|subscription|paper collection|source collection|corpus|download)\b/i.test(normalized)
|
|
137
|
+
|| /PDF|원문|전문|기관\s*구독|기관구독|구독|VPN|프록시|도서관|라이브러리|TDM|논문\s*수집|문헌\s*수집|코퍼스|다운로드/.test(normalized);
|
|
138
|
+
}
|
|
134
139
|
function looksLikeQuestionGenerationPrompt(prompt) {
|
|
135
140
|
return /\b(needed questions?|necessary questions?|question generation|clarifying questions?|ask questions?)\b/i.test(prompt)
|
|
136
141
|
|| /필요한\s*질문|질문을\s*(모두|많이|생성)|질문\s*생성|물어봐|질문해/.test(prompt);
|
|
@@ -184,11 +189,12 @@ function shouldSurfaceInterviewContext(prompt) {
|
|
|
184
189
|
return looksLikeExplicitInterviewPrompt(prompt) || looksLikeResearchStateConfirmationPrompt(prompt);
|
|
185
190
|
}
|
|
186
191
|
function shouldCreateRequiredQuestionsForPrompt(prompt) {
|
|
187
|
-
return !looksLikeLongTableProductOrToolingPrompt(prompt) &&
|
|
192
|
+
return !looksLikeLongTableProductOrToolingPrompt(prompt) &&
|
|
193
|
+
(looksLikeResearchCommitmentPrompt(prompt) || looksLikeAccessSensitiveResearchAction(prompt));
|
|
188
194
|
}
|
|
189
195
|
function shouldApplyProtectedDecisionClosure(runtime, prompt) {
|
|
190
196
|
return Boolean(runtime.context.session.protectedDecision) &&
|
|
191
|
-
|
|
197
|
+
looksLikeResearchCommitmentPrompt(prompt) &&
|
|
192
198
|
!looksLikeQuestionGenerationPrompt(prompt) &&
|
|
193
199
|
!looksLikeMultiCommitmentChangePrompt(prompt);
|
|
194
200
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { DecisionRecord, InvocationRecord, LongTableQuestionObligation, ProviderKind, QuestionOption, QuestionGenerationResult, QuestionOpportunity, QuestionSurface, QuestionRecord, ResearchState } from "@longtable/core";
|
|
1
|
+
import type { DecisionRecord, InvocationRecord, LongTableQuestionObligation, ProviderKind, QuestionOption, QuestionCommitmentFamily, QuestionEpistemicBasis, QuestionGenerationResult, QuestionOpportunity, QuestionSurface, QuestionRecord, ResearchState } from "@longtable/core";
|
|
2
2
|
import type { SetupPersistedOutput } from "@longtable/setup";
|
|
3
3
|
export type ProjectDisagreementPreference = "synthesis_only" | "show_on_conflict" | "always_visible";
|
|
4
4
|
export type StartInterviewSignal = "phenomenon" | "audience" | "artifact" | "evidence" | "assumption" | "decision_risk" | "voice";
|
|
@@ -221,6 +221,8 @@ export interface LongTableWorkspaceInspection {
|
|
|
221
221
|
id: string;
|
|
222
222
|
title: string;
|
|
223
223
|
question: string;
|
|
224
|
+
commitmentFamily?: QuestionCommitmentFamily;
|
|
225
|
+
epistemicBasis?: QuestionEpistemicBasis;
|
|
224
226
|
options: string[];
|
|
225
227
|
required: boolean;
|
|
226
228
|
}>;
|
|
@@ -235,6 +237,8 @@ export interface LongTableWorkspaceInspection {
|
|
|
235
237
|
id: string;
|
|
236
238
|
checkpointKey: string;
|
|
237
239
|
summary: string;
|
|
240
|
+
commitmentFamily?: QuestionCommitmentFamily;
|
|
241
|
+
epistemicBasis?: QuestionEpistemicBasis;
|
|
238
242
|
selectedOption?: string;
|
|
239
243
|
timestamp: string;
|
|
240
244
|
}>;
|
|
@@ -329,6 +333,8 @@ export declare function createWorkspaceQuestion(options: {
|
|
|
329
333
|
displayReason?: string;
|
|
330
334
|
provider?: ProviderKind;
|
|
331
335
|
required?: boolean;
|
|
336
|
+
commitmentFamily?: QuestionCommitmentFamily;
|
|
337
|
+
epistemicBasis?: QuestionEpistemicBasis;
|
|
332
338
|
}): Promise<{
|
|
333
339
|
question: QuestionRecord;
|
|
334
340
|
state: ResearchState;
|
package/dist/project-session.js
CHANGED
|
@@ -140,6 +140,15 @@ function renderResearchSpecificationSummary(specification, locale) {
|
|
|
140
140
|
if (specification.methodAnalysis.analysisOptions.length > 0) {
|
|
141
141
|
lines.push(`- ${korean ? "분석 옵션" : "Analysis options"}: ${specification.methodAnalysis.analysisOptions.join("; ")}`);
|
|
142
142
|
}
|
|
143
|
+
if (specification.evidenceAccess.requiredSources?.length) {
|
|
144
|
+
lines.push(`- ${korean ? "필요 근거원" : "Required sources"}: ${specification.evidenceAccess.requiredSources.join("; ")}`);
|
|
145
|
+
}
|
|
146
|
+
if (specification.evidenceAccess.accessRequirements?.length) {
|
|
147
|
+
lines.push(`- ${korean ? "Corpus and Access Plan" : "Corpus and Access Plan"}: ${specification.evidenceAccess.accessRequirements.join("; ")}`);
|
|
148
|
+
}
|
|
149
|
+
if (specification.evidenceAccess.evidenceStandards?.length) {
|
|
150
|
+
lines.push(`- ${korean ? "근거 기준" : "Evidence standards"}: ${specification.evidenceAccess.evidenceStandards.join("; ")}`);
|
|
151
|
+
}
|
|
143
152
|
if (specification.epistemicAlignment.conflictResolutionRule) {
|
|
144
153
|
lines.push(`- ${korean ? "충돌 조정 규칙" : "Conflict rule"}: ${specification.epistemicAlignment.conflictResolutionRule}`);
|
|
145
154
|
}
|
|
@@ -154,6 +163,43 @@ function renderResearchSpecificationSummary(specification, locale) {
|
|
|
154
163
|
}
|
|
155
164
|
return lines;
|
|
156
165
|
}
|
|
166
|
+
function renderResearchSpecificationStatus(session, locale) {
|
|
167
|
+
if (!session.firstResearchShape && !session.researchSpecification) {
|
|
168
|
+
return [];
|
|
169
|
+
}
|
|
170
|
+
const korean = locale === "ko";
|
|
171
|
+
if (!session.researchSpecification) {
|
|
172
|
+
return [
|
|
173
|
+
"",
|
|
174
|
+
korean ? "## Research Specification 상태" : "## Research Specification Status",
|
|
175
|
+
korean
|
|
176
|
+
? "- 상태: First Research Shape는 있지만 Research Specification은 아직 없습니다."
|
|
177
|
+
: "- Status: First Research Shape exists, but Research Specification is missing.",
|
|
178
|
+
korean
|
|
179
|
+
? "- 의미: First Research Shape는 짧은 핸들/재개 인덱스이며, 인터뷰 종료나 연구 명세 확정이 아닙니다."
|
|
180
|
+
: "- Meaning: First Research Shape is a short handle/resume index, not interview closure or a confirmed research specification.",
|
|
181
|
+
korean
|
|
182
|
+
? "- 다음 프로토콜: 충분한 내용이 있으면 `summarize_research_specification`으로 preview를 만들고 `confirm_research_specification`으로 저장/한 질문 더/섹션 수정/열어두기를 확인합니다."
|
|
183
|
+
: "- Next protocol: when enough detail exists, run `summarize_research_specification` to create the preview, then `confirm_research_specification` to confirm, ask one more question, revise a section, or keep it open."
|
|
184
|
+
];
|
|
185
|
+
}
|
|
186
|
+
const status = session.researchSpecification.confirmedAt
|
|
187
|
+
? "confirmed"
|
|
188
|
+
: session.researchSpecification.status ?? "draft";
|
|
189
|
+
if (status === "confirmed") {
|
|
190
|
+
return [];
|
|
191
|
+
}
|
|
192
|
+
return [
|
|
193
|
+
"",
|
|
194
|
+
korean ? "## Research Specification 상태" : "## Research Specification Status",
|
|
195
|
+
korean
|
|
196
|
+
? `- 상태: ${status}. Research Specification은 저장되어 있지만 아직 확정된 종료 지점이 아닙니다.`
|
|
197
|
+
: `- Status: ${status}. Research Specification exists, but it is not a confirmed closure point yet.`,
|
|
198
|
+
korean
|
|
199
|
+
? "- 다음 프로토콜: 명세를 업데이트한 뒤 `confirm_research_specification`으로 다시 preview 확인을 받아야 합니다."
|
|
200
|
+
: "- Next protocol: update the specification, then return to `confirm_research_specification` for another preview confirmation."
|
|
201
|
+
];
|
|
202
|
+
}
|
|
157
203
|
function buildCurrentGuide(project, session, recentInvocations = [], pendingQuestions = [], pendingObligations = []) {
|
|
158
204
|
const locale = normalizeLocale(session.locale ?? project.locale);
|
|
159
205
|
const openQuestions = session.openQuestions && session.openQuestions.length > 0
|
|
@@ -182,6 +228,7 @@ function buildCurrentGuide(project, session, recentInvocations = [], pendingQues
|
|
|
182
228
|
`- 다음 액션: ${nextAction}`,
|
|
183
229
|
`- 관점: ${session.requestedPerspectives.length > 0 ? session.requestedPerspectives.join(", ") : "auto"}`,
|
|
184
230
|
`- disagreement: ${session.disagreementPreference}`,
|
|
231
|
+
...renderResearchSpecificationStatus(session, locale),
|
|
185
232
|
"",
|
|
186
233
|
"## 열린 질문",
|
|
187
234
|
...openQuestions.map((question) => `- ${question}`),
|
|
@@ -201,7 +248,7 @@ function buildCurrentGuide(project, session, recentInvocations = [], pendingQues
|
|
|
201
248
|
"## 대기 중인 결정 질문",
|
|
202
249
|
...pendingQuestions.map((record) => {
|
|
203
250
|
const options = formatQuestionOptionValues(record).join("/");
|
|
204
|
-
return `- ${record.id}: ${record.prompt.question} (${options})`;
|
|
251
|
+
return `- ${record.id}: ${record.prompt.question}${formatQuestionMetadata(record)} (${options})`;
|
|
205
252
|
}),
|
|
206
253
|
"- 답변 기록: `longtable decide --question <id> --answer <value>`"
|
|
207
254
|
]
|
|
@@ -257,6 +304,7 @@ function buildCurrentGuide(project, session, recentInvocations = [], pendingQues
|
|
|
257
304
|
`- Next action: ${nextAction}`,
|
|
258
305
|
`- Perspectives: ${session.requestedPerspectives.length > 0 ? session.requestedPerspectives.join(", ") : "auto"}`,
|
|
259
306
|
`- Disagreement: ${session.disagreementPreference}`,
|
|
307
|
+
...renderResearchSpecificationStatus(session, locale),
|
|
260
308
|
"",
|
|
261
309
|
"## Open Questions",
|
|
262
310
|
...openQuestions.map((question) => `- ${question}`),
|
|
@@ -276,7 +324,7 @@ function buildCurrentGuide(project, session, recentInvocations = [], pendingQues
|
|
|
276
324
|
"## Pending Decision Questions",
|
|
277
325
|
...pendingQuestions.map((record) => {
|
|
278
326
|
const options = formatQuestionOptionValues(record).join("/");
|
|
279
|
-
return `- ${record.id}: ${record.prompt.question} (${options})`;
|
|
327
|
+
return `- ${record.id}: ${record.prompt.question}${formatQuestionMetadata(record)} (${options})`;
|
|
280
328
|
}),
|
|
281
329
|
"- Record an answer: `longtable decide --question <id> --answer <value>`"
|
|
282
330
|
]
|
|
@@ -366,6 +414,13 @@ function formatQuestionOptionValues(record) {
|
|
|
366
414
|
}
|
|
367
415
|
return values;
|
|
368
416
|
}
|
|
417
|
+
function formatQuestionMetadata(record) {
|
|
418
|
+
const parts = [
|
|
419
|
+
record.commitmentFamily ? `commitment: ${record.commitmentFamily}` : "",
|
|
420
|
+
record.epistemicBasis ? `basis: ${record.epistemicBasis}` : ""
|
|
421
|
+
].filter(Boolean);
|
|
422
|
+
return parts.length > 0 ? ` [${parts.join("; ")}]` : "";
|
|
423
|
+
}
|
|
369
424
|
function summarizeWorkspaceInspection(context, state) {
|
|
370
425
|
const questions = state.questionLog ?? [];
|
|
371
426
|
const pendingQuestions = questions.filter((record) => record.status === "pending");
|
|
@@ -426,6 +481,8 @@ function summarizeWorkspaceInspection(context, state) {
|
|
|
426
481
|
id: record.id,
|
|
427
482
|
title: record.prompt.title,
|
|
428
483
|
question: record.prompt.question,
|
|
484
|
+
...(record.commitmentFamily ? { commitmentFamily: record.commitmentFamily } : {}),
|
|
485
|
+
...(record.epistemicBasis ? { epistemicBasis: record.epistemicBasis } : {}),
|
|
429
486
|
options: formatQuestionOptionValues(record),
|
|
430
487
|
required: record.prompt.required
|
|
431
488
|
})),
|
|
@@ -440,6 +497,8 @@ function summarizeWorkspaceInspection(context, state) {
|
|
|
440
497
|
id: record.id,
|
|
441
498
|
checkpointKey: record.checkpointKey,
|
|
442
499
|
summary: record.summary,
|
|
500
|
+
...(record.commitmentFamily ? { commitmentFamily: record.commitmentFamily } : {}),
|
|
501
|
+
...(record.epistemicBasis ? { epistemicBasis: record.epistemicBasis } : {}),
|
|
443
502
|
...(record.selectedOption ? { selectedOption: record.selectedOption } : {}),
|
|
444
503
|
timestamp: record.timestamp
|
|
445
504
|
})),
|
|
@@ -486,9 +545,12 @@ function buildProjectAgentsMd(project, session) {
|
|
|
486
545
|
"- Begin exploratory work with clarifying or tension questions before recommending a direction.",
|
|
487
546
|
"- For `$longtable-interview`, ask one natural-language question at a time, reflect with `LongTable hears: ...`, record turns when MCP is available, and avoid early reader/reviewer or theory/method/measurement classification.",
|
|
488
547
|
"- Do not summarize `$longtable-interview` because a fixed number of turns has passed; wait for content-based readiness around research object, focal uncertainty, boundary, evidence/material, protected decision, and next action.",
|
|
548
|
+
"- First Research Shape is a short handle/resume index, not the default closure point.",
|
|
489
549
|
"- After the First Research Shape, create a Research Specification when the interview has enough detail to preserve scope, construct ontology, theory framing, coding rules, method options, evidence/access requirements, epistemic alignment, protected decisions, open questions, and next actions.",
|
|
550
|
+
"- If a confirmed First Research Shape exists without a Research Specification, continue directly into the next Research Specification question instead of asking shape-level continue/revise/restart questions.",
|
|
551
|
+
"- If the researcher chooses `ask_one_more` or `revise_section` at Research Specification confirmation, answer that gap and return to the Research Specification Preview before ending the interview.",
|
|
490
552
|
"- Do not let unrelated pending Researcher Checkpoints interrupt `$longtable-interview`; mention them only as separate unresolved checkpoints unless the researcher is confirming, saving, or recording a research decision.",
|
|
491
|
-
"- Use structured options
|
|
553
|
+
"- Use structured options at the final Research Specification confirmation, at explicit short-handle stop points, or at true checkpoint boundaries.",
|
|
492
554
|
"- If you foreground role perspectives, disclose them with `LongTable consulted: ...`.",
|
|
493
555
|
"- Keep one accountable synthesis, but do not hide meaningful disagreement.",
|
|
494
556
|
...(session.disagreementPreference === "always_visible"
|
|
@@ -1219,8 +1281,8 @@ function questionPriority(spec) {
|
|
|
1219
1281
|
const requiredWeight = spec.kind === "research_commitment" || spec.required ? 20 : 0;
|
|
1220
1282
|
return (byKey[spec.key] ?? 0) + confidenceWeight + requiredWeight;
|
|
1221
1283
|
}
|
|
1222
|
-
function followUpQuestionOptions(first, second, third,
|
|
1223
|
-
return [first, second, third, ...
|
|
1284
|
+
function followUpQuestionOptions(first, second, third, ...rest) {
|
|
1285
|
+
return [first, second, third, ...rest];
|
|
1224
1286
|
}
|
|
1225
1287
|
export function buildQuestionOpportunitySpecs(prompt, options = {}) {
|
|
1226
1288
|
const normalized = prompt.toLowerCase();
|
|
@@ -1285,8 +1347,26 @@ export function buildQuestionOpportunitySpecs(prompt, options = {}) {
|
|
|
1285
1347
|
/\brandom[- ]?effects\b/i,
|
|
1286
1348
|
/분석\s*계획|분석\s*방법|메타\s*분석|분석\s*(?:모형|모델)|통계\s*(?:모형|모델)|구조\s*방정식|경로\s*모형|조절효과|랜덤\s*효과/
|
|
1287
1349
|
]);
|
|
1350
|
+
const accessCue = includesAny(normalized, [
|
|
1351
|
+
/\b(pdf|full[- ]?text|tdm|publisher api|institutional access|library login|vpn|proxy|subscription|paper collection|source collection|corpus|download)\b/i,
|
|
1352
|
+
/PDF|원문|전문|기관\s*구독|기관구독|구독|VPN|프록시|도서관|라이브러리|TDM|논문\s*수집|문헌\s*수집|코퍼스|다운로드/
|
|
1353
|
+
]);
|
|
1288
1354
|
const decisionFamilyCount = [scopeCue, theoryCue, measurementCodingCue, methodCue, analysisCue]
|
|
1289
1355
|
.filter(Boolean).length;
|
|
1356
|
+
if (accessCue) {
|
|
1357
|
+
push({
|
|
1358
|
+
key: "scholarly_access_policy",
|
|
1359
|
+
kind: "evidence_risk",
|
|
1360
|
+
title: "Scholarly access policy",
|
|
1361
|
+
question: "What scholarly access route should LongTable use before collecting PDFs, full text, or subscription-only evidence?",
|
|
1362
|
+
whyNow: "Full-text access decisions can change the corpus, inclusion bias, reproducibility, and TDM permission boundary.",
|
|
1363
|
+
options: followUpQuestionOptions({ value: "oa_only", label: "OA-only", description: "Use only open-access PDF or full text.", recommended: true }, { value: "institutional_access", label: "Institutional access", description: "Include VPN/proxy/library-login access after the researcher completes login." }, { value: "publisher_tdm", label: "Publisher API/TDM", description: "Use configured publisher API/TDM credentials and record entitlement checks." }, { value: "manual_pdf", label: "Manual PDFs", description: "Use PDFs supplied by the researcher and record provenance." }, { value: "metadata_only", label: "Metadata only", description: "Do not collect full text yet." }),
|
|
1364
|
+
confidence: "high",
|
|
1365
|
+
autoEligible: true,
|
|
1366
|
+
required: true,
|
|
1367
|
+
cues: ["scholarly_access", "full_text", "corpus"]
|
|
1368
|
+
});
|
|
1369
|
+
}
|
|
1290
1370
|
if (decisionActionCue && decisionFamilyCount >= 2) {
|
|
1291
1371
|
push({
|
|
1292
1372
|
key: "research_direction_change_commitment",
|
|
@@ -1619,7 +1699,7 @@ export function buildQuestionOpportunitySpecs(prompt, options = {}) {
|
|
|
1619
1699
|
}
|
|
1620
1700
|
let selected = options.autoOnly === true ? specs.filter((spec) => spec.autoEligible) : specs;
|
|
1621
1701
|
if (options.requiredOnly === true) {
|
|
1622
|
-
selected = selected.filter((spec) => spec.kind === "research_commitment");
|
|
1702
|
+
selected = selected.filter((spec) => spec.kind === "research_commitment" || spec.required);
|
|
1623
1703
|
}
|
|
1624
1704
|
if (normalized.includes("protected decision closure pressure")) {
|
|
1625
1705
|
selected = selected.filter((spec) => spec.key === "protected_decision_closure");
|
|
@@ -1641,6 +1721,70 @@ export function generateQuestionOpportunities(prompt, options = {}) {
|
|
|
1641
1721
|
};
|
|
1642
1722
|
}
|
|
1643
1723
|
const FOLLOW_UP_PROMPT_PREFIX = "Follow-up prompt:";
|
|
1724
|
+
function compactMetadataText(parts) {
|
|
1725
|
+
return parts
|
|
1726
|
+
.flatMap((part) => Array.isArray(part) ? part : [part])
|
|
1727
|
+
.filter((part) => Boolean(part && part.trim()))
|
|
1728
|
+
.join(" ")
|
|
1729
|
+
.replace(/\s+/g, " ")
|
|
1730
|
+
.trim()
|
|
1731
|
+
.toLowerCase();
|
|
1732
|
+
}
|
|
1733
|
+
function textMatchesAny(text, patterns) {
|
|
1734
|
+
return patterns.some((pattern) => pattern.test(text));
|
|
1735
|
+
}
|
|
1736
|
+
const COMMITMENT_FAMILY_BY_CHECKPOINT = [
|
|
1737
|
+
[/product|meta_decision/, "product_policy"],
|
|
1738
|
+
[/research_question|research_direction|scope|boundary|inclusion|exclusion/, "scope"],
|
|
1739
|
+
[/theory|construct|conceptual/, "construct"],
|
|
1740
|
+
[/measurement|coding|codebook|extraction/, "coding"],
|
|
1741
|
+
[/method|analysis|panel_disagreement|team_debate|review/, "method"],
|
|
1742
|
+
[/evidence|scholarly_access|source_authority/, "evidence"],
|
|
1743
|
+
[/knowledge_gap|tacit_assumption|epistemic/, "epistemic_authority"]
|
|
1744
|
+
];
|
|
1745
|
+
function inferCommitmentFamily(input) {
|
|
1746
|
+
const checkpointKey = (input.checkpointKey ?? "").toLowerCase();
|
|
1747
|
+
const matched = COMMITMENT_FAMILY_BY_CHECKPOINT.find(([pattern]) => pattern.test(checkpointKey));
|
|
1748
|
+
if (matched)
|
|
1749
|
+
return matched[1];
|
|
1750
|
+
if (input.triggerFamily === "meta_decision")
|
|
1751
|
+
return "product_policy";
|
|
1752
|
+
if (input.triggerFamily === "evidence")
|
|
1753
|
+
return "evidence";
|
|
1754
|
+
const text = compactMetadataText([input.title, input.question, input.prompt, input.rationale]);
|
|
1755
|
+
if (textMatchesAny(text, [/checkpoint policy/, /hook ux/, /product language/, /\breadme\b/, /제품 언어|체크포인트 정책|훅|리드미/])) {
|
|
1756
|
+
return "product_policy";
|
|
1757
|
+
}
|
|
1758
|
+
return undefined;
|
|
1759
|
+
}
|
|
1760
|
+
function inferEpistemicBasis(input) {
|
|
1761
|
+
const text = compactMetadataText([input.title, input.question, input.prompt, input.rationale]);
|
|
1762
|
+
const bases = [];
|
|
1763
|
+
if (textMatchesAny(text, [/\bresearcher\b/, /\bhuman\b/, /\byour judgment\b/, /\byour knowledge\b/, /연구자|인간|사람|너의\s*판단|당신의\s*판단|내\s*지식|사용자/])) {
|
|
1764
|
+
bases.push("researcher_knowledge");
|
|
1765
|
+
}
|
|
1766
|
+
if (textMatchesAny(text, [/\bproject state\b/, /\bworkspace\b/, /\bcurrent\.md\b/, /\.longtable\b/, /\bstate\.json\b/, /\bdataset\b/, /\bcodebook\b/, /\bcoding sheet\b/, /프로젝트\s*상태|워크스페이스|데이터셋|코드북|코딩\s*시트/])) {
|
|
1767
|
+
bases.push("project_state");
|
|
1768
|
+
}
|
|
1769
|
+
if (textMatchesAny(text, [/\bexternal evidence\b/, /\bliterature\b/, /\bpaper\b/, /\bpdf\b/, /\bsource\b/, /\bcitation\b/, /\breference\b/, /\bfull[- ]?text\b/, /외부\s*근거|문헌|논문|원문|전문|출처|인용|레퍼런스/])) {
|
|
1770
|
+
bases.push("external_evidence");
|
|
1771
|
+
}
|
|
1772
|
+
if (textMatchesAny(text, [/\bcodex\b/, /\bllm\b/, /\blanguage model\b/, /\bmodel judgment\b/, /\bai inference\b/, /\bassistant judgment\b/, /코덱스|언어\s*모델|모델\s*판단|AI\s*추론|LLM/])) {
|
|
1773
|
+
bases.push("ai_inference");
|
|
1774
|
+
}
|
|
1775
|
+
const unique = [...new Set(bases)];
|
|
1776
|
+
if (unique.length > 1)
|
|
1777
|
+
return "mixed";
|
|
1778
|
+
return unique[0];
|
|
1779
|
+
}
|
|
1780
|
+
function resolveQuestionRecordMetadata(input) {
|
|
1781
|
+
const commitmentFamily = input.commitmentFamily ?? inferCommitmentFamily(input);
|
|
1782
|
+
const epistemicBasis = input.epistemicBasis ?? inferEpistemicBasis(input);
|
|
1783
|
+
return {
|
|
1784
|
+
...(commitmentFamily ? { commitmentFamily } : {}),
|
|
1785
|
+
...(epistemicBasis ? { epistemicBasis } : {})
|
|
1786
|
+
};
|
|
1787
|
+
}
|
|
1644
1788
|
function hasFollowUpPrompt(record, prompt) {
|
|
1645
1789
|
return record.prompt.rationale.includes(`${FOLLOW_UP_PROMPT_PREFIX} ${prompt}`);
|
|
1646
1790
|
}
|
|
@@ -1678,31 +1822,43 @@ export async function createWorkspaceFollowUpQuestions(options) {
|
|
|
1678
1822
|
if (specsToCreate.length === 0) {
|
|
1679
1823
|
return { questions: pendingMatches, state, created: false, alreadyAnswered: false };
|
|
1680
1824
|
}
|
|
1681
|
-
const questions = specsToCreate.map((spec) =>
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1825
|
+
const questions = specsToCreate.map((spec) => {
|
|
1826
|
+
const checkpointKey = `follow_up_${spec.key}`;
|
|
1827
|
+
const rationale = [
|
|
1828
|
+
spec.whyNow,
|
|
1829
|
+
`Question kind: ${spec.kind}`,
|
|
1830
|
+
`Question confidence: ${spec.confidence}`,
|
|
1831
|
+
`${FOLLOW_UP_PROMPT_PREFIX} ${options.prompt}`
|
|
1832
|
+
];
|
|
1833
|
+
const metadata = resolveQuestionRecordMetadata({
|
|
1834
|
+
checkpointKey,
|
|
1689
1835
|
title: spec.title,
|
|
1690
1836
|
question: spec.question,
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1837
|
+
prompt: options.prompt,
|
|
1838
|
+
rationale
|
|
1839
|
+
});
|
|
1840
|
+
return {
|
|
1841
|
+
id: createId("question_record"),
|
|
1842
|
+
createdAt,
|
|
1843
|
+
updatedAt: createdAt,
|
|
1844
|
+
status: "pending",
|
|
1845
|
+
...metadata,
|
|
1846
|
+
prompt: {
|
|
1847
|
+
id: createId("question_prompt"),
|
|
1848
|
+
checkpointKey,
|
|
1849
|
+
title: spec.title,
|
|
1850
|
+
question: spec.question,
|
|
1851
|
+
type: "single_choice",
|
|
1852
|
+
options: spec.options,
|
|
1853
|
+
allowOther: true,
|
|
1854
|
+
otherLabel: "Other",
|
|
1855
|
+
required: options.required ?? spec.required,
|
|
1856
|
+
source: "runtime_guidance",
|
|
1857
|
+
rationale,
|
|
1858
|
+
preferredSurfaces: preferredSurfaces
|
|
1859
|
+
}
|
|
1860
|
+
};
|
|
1861
|
+
});
|
|
1706
1862
|
const updated = appendQuestionRecords(state, questions);
|
|
1707
1863
|
await writeFile(options.context.stateFilePath, JSON.stringify(updated, null, 2), "utf8");
|
|
1708
1864
|
await syncCurrentWorkspaceView(options.context);
|
|
@@ -1716,16 +1872,35 @@ export async function createWorkspaceQuestion(options) {
|
|
|
1716
1872
|
});
|
|
1717
1873
|
const checkpointKey = options.checkpointKey ?? trigger.signal.checkpointKey;
|
|
1718
1874
|
const createdAt = nowIso();
|
|
1875
|
+
const title = options.title ?? questionTitleForCheckpoint(trigger.family, checkpointKey);
|
|
1876
|
+
const questionText = options.question ?? questionTextForCheckpoint(trigger.family, options.prompt, checkpointKey);
|
|
1877
|
+
const rationale = [
|
|
1878
|
+
...trigger.rationale,
|
|
1879
|
+
`Trigger family: ${trigger.family}.`,
|
|
1880
|
+
`Trigger confidence: ${trigger.confidence}.`,
|
|
1881
|
+
`Original prompt: ${options.prompt}`
|
|
1882
|
+
];
|
|
1883
|
+
const metadata = resolveQuestionRecordMetadata({
|
|
1884
|
+
checkpointKey,
|
|
1885
|
+
triggerFamily: trigger.family,
|
|
1886
|
+
title,
|
|
1887
|
+
question: questionText,
|
|
1888
|
+
prompt: options.prompt,
|
|
1889
|
+
rationale,
|
|
1890
|
+
commitmentFamily: options.commitmentFamily,
|
|
1891
|
+
epistemicBasis: options.epistemicBasis
|
|
1892
|
+
});
|
|
1719
1893
|
const question = {
|
|
1720
1894
|
id: createId("question_record"),
|
|
1721
1895
|
createdAt,
|
|
1722
1896
|
updatedAt: createdAt,
|
|
1723
1897
|
status: "pending",
|
|
1898
|
+
...metadata,
|
|
1724
1899
|
prompt: {
|
|
1725
1900
|
id: createId("question_prompt"),
|
|
1726
1901
|
checkpointKey,
|
|
1727
|
-
title
|
|
1728
|
-
question:
|
|
1902
|
+
title,
|
|
1903
|
+
question: questionText,
|
|
1729
1904
|
type: "single_choice",
|
|
1730
1905
|
options: options.questionOptions ?? optionsForCheckpointTrigger(trigger.family, checkpointKey),
|
|
1731
1906
|
allowOther: true,
|
|
@@ -1733,12 +1908,7 @@ export async function createWorkspaceQuestion(options) {
|
|
|
1733
1908
|
required: options.required ?? trigger.requiresQuestionBeforeClosure,
|
|
1734
1909
|
source: "checkpoint",
|
|
1735
1910
|
displayReason: options.displayReason ?? trigger.rationale[0],
|
|
1736
|
-
rationale
|
|
1737
|
-
...trigger.rationale,
|
|
1738
|
-
`Trigger family: ${trigger.family}.`,
|
|
1739
|
-
`Trigger confidence: ${trigger.confidence}.`,
|
|
1740
|
-
`Original prompt: ${options.prompt}`
|
|
1741
|
-
],
|
|
1911
|
+
rationale,
|
|
1742
1912
|
preferredSurfaces: options.provider === "claude"
|
|
1743
1913
|
? ["native_structured", "numbered"]
|
|
1744
1914
|
: ["mcp_elicitation", "numbered"]
|
|
@@ -1865,6 +2035,8 @@ export async function answerWorkspaceQuestion(options) {
|
|
|
1865
2035
|
level: question.prompt.required ? "adaptive_required" : "recommended",
|
|
1866
2036
|
mode: "commit",
|
|
1867
2037
|
summary: `Answered ${question.prompt.title}: ${answer.selectedLabels.join(", ")}`,
|
|
2038
|
+
...(question.commitmentFamily ? { commitmentFamily: question.commitmentFamily } : {}),
|
|
2039
|
+
...(question.epistemicBasis ? { epistemicBasis: question.epistemicBasis } : {}),
|
|
1868
2040
|
selectedOption: answer.selectedValues[0],
|
|
1869
2041
|
...(rationale ? { rationale } : {})
|
|
1870
2042
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type CrossrefTdmDiscovery, type EvidenceCard, type Publisher, type PublisherAccessRecord, type PublisherProbeInput, type PublisherProbeTarget, type
|
|
1
|
+
import { type CrossrefTdmDiscovery, type EvidenceCard, type Publisher, type PublisherAccessRecord, type PublisherProbeInput, type PublisherProbeTarget, type SearchFetch } from "./types.js";
|
|
2
2
|
interface PublisherConfig {
|
|
3
3
|
publisher: Publisher;
|
|
4
4
|
label: string;
|
|
@@ -12,8 +12,6 @@ export declare function discoverCrossrefTdm(doi: string, env?: Record<string, st
|
|
|
12
12
|
export declare function publisherConfigs(): PublisherConfig[];
|
|
13
13
|
export declare function probePublisherAccess(input: PublisherProbeInput): Promise<PublisherAccessRecord>;
|
|
14
14
|
export declare function summarizeConfiguredPublisherAccess(env?: Record<string, string | undefined>): PublisherAccessRecord[];
|
|
15
|
-
export declare function buildSearchCapabilitySnapshot(records: PublisherAccessRecord[], env?: Record<string, string | undefined>): SearchCapabilitySnapshot;
|
|
16
|
-
export declare function searchCapabilitySnapshotPath(home?: string): string;
|
|
17
15
|
export declare function enrichCardsWithPublisherAccess(input: {
|
|
18
16
|
cards: EvidenceCard[];
|
|
19
17
|
env?: Record<string, string | undefined>;
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { join } from "node:path";
|
|
2
|
-
import { homedir } from "node:os";
|
|
3
1
|
import { PUBLISHERS } from "./types.js";
|
|
4
2
|
const PUBLISHER_CONFIGS = {
|
|
5
3
|
elsevier: {
|
|
@@ -505,17 +503,6 @@ export function summarizeConfiguredPublisherAccess(env = process.env) {
|
|
|
505
503
|
});
|
|
506
504
|
});
|
|
507
505
|
}
|
|
508
|
-
export function buildSearchCapabilitySnapshot(records, env = process.env) {
|
|
509
|
-
return {
|
|
510
|
-
version: 1,
|
|
511
|
-
updatedAt: now(),
|
|
512
|
-
contactEmailPresent: Boolean(env.LONGTABLE_CONTACT_EMAIL?.trim()),
|
|
513
|
-
records
|
|
514
|
-
};
|
|
515
|
-
}
|
|
516
|
-
export function searchCapabilitySnapshotPath(home = homedir()) {
|
|
517
|
-
return join(home, ".longtable", "search-capabilities.json");
|
|
518
|
-
}
|
|
519
506
|
function bestAccessStatus(record) {
|
|
520
507
|
if (record.entitlementStatus === "licensed_full_text_available" && record.collectionDepth === "licensed_snippet") {
|
|
521
508
|
return "licensed_full_text_checked";
|
package/dist/search/types.d.ts
CHANGED
|
@@ -173,12 +173,6 @@ export interface PublisherAccessRecord {
|
|
|
173
173
|
evidenceSnippet?: string;
|
|
174
174
|
crossref?: CrossrefTdmDiscovery;
|
|
175
175
|
}
|
|
176
|
-
export interface SearchCapabilitySnapshot {
|
|
177
|
-
version: 1;
|
|
178
|
-
updatedAt: string;
|
|
179
|
-
contactEmailPresent: boolean;
|
|
180
|
-
records: PublisherAccessRecord[];
|
|
181
|
-
}
|
|
182
176
|
export interface PublisherProbeInput {
|
|
183
177
|
doi: string;
|
|
184
178
|
publisher?: PublisherProbeTarget;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@longtable/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.47",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Researcher-facing LongTable CLI",
|
|
6
6
|
"type": "module",
|
|
@@ -29,12 +29,12 @@
|
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@clack/prompts": "^1.2.0",
|
|
32
|
-
"@longtable/checkpoints": "0.1.
|
|
33
|
-
"@longtable/core": "0.1.
|
|
34
|
-
"@longtable/memory": "0.1.
|
|
35
|
-
"@longtable/provider-claude": "0.1.
|
|
36
|
-
"@longtable/provider-codex": "0.1.
|
|
37
|
-
"@longtable/setup": "0.1.
|
|
32
|
+
"@longtable/checkpoints": "0.1.47",
|
|
33
|
+
"@longtable/core": "0.1.47",
|
|
34
|
+
"@longtable/memory": "0.1.47",
|
|
35
|
+
"@longtable/provider-claude": "0.1.47",
|
|
36
|
+
"@longtable/provider-codex": "0.1.47",
|
|
37
|
+
"@longtable/setup": "0.1.47"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@types/node": "^22.10.1",
|