@longtable/cli 0.1.44 → 0.1.45

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 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
- Publisher access is configured separately through environment variables and
273
- DOI probes. `longtable search setup` checks Elsevier, Springer Nature, Wiley,
274
- and Taylor & Francis credentials or TDM tokens without storing secrets.
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 search setup
281
- longtable search probe --doi "10.1016/example" --publisher elsevier
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, buildSearchCapabilitySnapshot, parsePublisherTarget, probePublisherAccess, publisherConfigs, runResearchSearch, searchCapabilitySnapshotPath, summarizeConfiguredPublisherAccess } from "./search/index.js";
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";
@@ -109,10 +109,11 @@ function usage() {
109
109
  " longtable show [--json] [--path <file>]",
110
110
  " longtable install [--json] [--path <file>] [--runtime-path <file>]",
111
111
  " longtable mcp install [--provider codex|claude|all] [--write] [--checkpoint-ui off|interactive|strong] [--json] [--codex-config <path>] [--claude-settings <path>] [--package <spec>]",
112
+ " longtable access setup [--doi <doi>] [--json]",
113
+ " longtable access status [--json]",
114
+ " longtable access doctor [--doi <doi>] [--publisher auto|elsevier|springer_nature|wiley|taylor_francis|all] [--json]",
115
+ " longtable access probe --doi <doi> [--publisher auto|elsevier|springer_nature|wiley|taylor_francis] [--json]",
112
116
  " 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
117
  " longtable sentinel --prompt <text> [--cwd <path>] [--json] [--record]",
117
118
  " longtable team --prompt <text> [--role <role[,role]>] [--debate] [--rounds 3|5] [--cwd <path>] [--json]",
118
119
  " longtable ask [--prompt <text>] [--print] [--json] [--setup <path>] [--cwd <path>]",
@@ -154,7 +155,7 @@ function parseArgs(argv) {
154
155
  const values = {};
155
156
  let subcommand = maybeSubcommand;
156
157
  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);
158
+ 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
159
  let startIndex = 1;
159
160
  if (modeCommand) {
160
161
  subcommand = undefined;
@@ -163,7 +164,7 @@ function parseArgs(argv) {
163
164
  else if (command === "codex" || command === "claude" || command === "mcp") {
164
165
  startIndex = 2;
165
166
  }
166
- else if (command === "search" && maybeSubcommand && !maybeSubcommand.startsWith("--")) {
167
+ else if ((command === "access" || command === "search") && maybeSubcommand && !maybeSubcommand.startsWith("--")) {
167
168
  subcommand = maybeSubcommand;
168
169
  startIndex = 2;
169
170
  }
@@ -1789,6 +1790,11 @@ const QUESTION_AUDIT_FIXTURES = [
1789
1790
  prompt: "Protected decision closure pressure: measurement. User prompt: Implement the plan.",
1790
1791
  expectedKinds: ["research_commitment"]
1791
1792
  },
1793
+ {
1794
+ id: "scholarly_access_policy",
1795
+ prompt: "메타분석 논문들의 PDF와 full text를 수집해서 원문 기반으로 코딩해줘.",
1796
+ expectedKinds: ["evidence_risk"]
1797
+ },
1792
1798
  {
1793
1799
  id: "low_stakes_copyedit",
1794
1800
  prompt: "문장 끝 공백만 정리해줘.",
@@ -2347,6 +2353,86 @@ async function recordEvidenceRun(run, workingDirectory) {
2347
2353
  await syncCurrentWorkspaceView(context);
2348
2354
  return evidencePath;
2349
2355
  }
2356
+ function nowIso() {
2357
+ return new Date().toISOString();
2358
+ }
2359
+ function uniqueAccessRoutes(routes) {
2360
+ return [...new Set(routes)];
2361
+ }
2362
+ function accessReadinessPath(home = homedir()) {
2363
+ return join(home, ".longtable", "access-readiness.json");
2364
+ }
2365
+ function readAccessReadinessProfile() {
2366
+ const path = accessReadinessPath();
2367
+ if (!existsSync(path)) {
2368
+ return undefined;
2369
+ }
2370
+ try {
2371
+ return JSON.parse(readFileSync(path, "utf8"));
2372
+ }
2373
+ catch {
2374
+ return undefined;
2375
+ }
2376
+ }
2377
+ function hasPublisherCredentialSignal(records) {
2378
+ return records.some((record) => record.presentEnv.length > 0 || record.credentialStatus !== "missing");
2379
+ }
2380
+ function readinessPublisherRecord(record) {
2381
+ const safeRecord = { ...record };
2382
+ delete safeRecord.evidenceSnippet;
2383
+ return safeRecord;
2384
+ }
2385
+ function inferAccessRoutes(records) {
2386
+ const routes = ["metadata"];
2387
+ if (hasPublisherCredentialSignal(records)) {
2388
+ routes.push("publisher_tdm");
2389
+ }
2390
+ return routes;
2391
+ }
2392
+ function buildAccessReadinessProfile(options) {
2393
+ const publisherRecords = options.publisherRecords ?? summarizeConfiguredPublisherAccess(env);
2394
+ const routes = uniqueAccessRoutes(options.routes?.length ? options.routes : inferAccessRoutes(publisherRecords));
2395
+ const disabled = options.readiness === "disabled";
2396
+ const institutionalMode = options.institutionalAccessMode;
2397
+ const institutionalAccess = routes.includes("institutional")
2398
+ ? {
2399
+ available: true,
2400
+ mode: institutionalMode ?? "unknown",
2401
+ verified: false,
2402
+ note: "LongTable records institutional access readiness only. The researcher must complete VPN/proxy/library login directly."
2403
+ }
2404
+ : undefined;
2405
+ return {
2406
+ version: 1,
2407
+ updatedAt: nowIso(),
2408
+ readiness: options.readiness,
2409
+ metadataSources: [...SEARCH_SOURCES],
2410
+ routes: disabled ? [] : routes,
2411
+ ...(institutionalAccess ? { institutionalAccess } : {}),
2412
+ publisherTdm: disabled
2413
+ ? "deferred"
2414
+ : routes.includes("publisher_tdm")
2415
+ ? hasPublisherCredentialSignal(publisherRecords) ? "configured" : "unknown"
2416
+ : "not_configured",
2417
+ oaOnly: !disabled && routes.includes("oa_full_text") && !routes.includes("institutional") && !routes.includes("publisher_tdm"),
2418
+ manualPdfAllowed: !disabled && routes.includes("manual_pdf"),
2419
+ storesSecrets: false,
2420
+ requiresCheckpointBeforeSearch: options.readiness === "deferred",
2421
+ requiresCheckpointBeforeFullText: options.readiness !== "disabled",
2422
+ ...(disabled ? {} : {
2423
+ publisherAccess: {
2424
+ contactEmailPresent: Boolean(env.LONGTABLE_CONTACT_EMAIL?.trim()),
2425
+ records: publisherRecords.map(readinessPublisherRecord)
2426
+ }
2427
+ })
2428
+ };
2429
+ }
2430
+ async function saveAccessReadinessProfile(profile) {
2431
+ const profilePath = accessReadinessPath();
2432
+ await mkdir(dirname(profilePath), { recursive: true });
2433
+ await writeJsonFile(profilePath, profile);
2434
+ return profilePath;
2435
+ }
2350
2436
  function renderPublisherAccessRecord(record) {
2351
2437
  const envSummary = record.missingEnv.length > 0
2352
2438
  ? `missing ${record.missingEnv.join(", ")}`
@@ -2381,12 +2467,6 @@ function renderPublisherAccessRecords(title, records, capabilityPath) {
2381
2467
  }
2382
2468
  return lines.join("\n");
2383
2469
  }
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
2470
  async function probeAllPublishers(doi) {
2391
2471
  const records = [];
2392
2472
  for (const publisher of publisherConfigs()) {
@@ -2398,9 +2478,48 @@ async function probeAllPublishers(doi) {
2398
2478
  }
2399
2479
  return records;
2400
2480
  }
2401
- async function runSearchProbe(args) {
2481
+ function renderAccessReadinessProfile(profile, profilePath = accessReadinessPath()) {
2482
+ const lines = [
2483
+ "LongTable Scholarly Access Readiness",
2484
+ `- profile: ${profilePath}`,
2485
+ `- readiness: ${profile.readiness}`,
2486
+ `- routes: ${profile.routes.length > 0 ? profile.routes.join(", ") : "none"}`,
2487
+ `- metadata sources: ${profile.metadataSources.join(", ")}`,
2488
+ `- OA-only: ${profile.oaOnly ? "yes" : "no"}`,
2489
+ `- manual PDF allowed: ${profile.manualPdfAllowed ? "yes" : "no"}`,
2490
+ `- publisher/TDM: ${profile.publisherTdm}`,
2491
+ `- stores secrets: ${profile.storesSecrets ? "yes" : "no"}`,
2492
+ `- checkpoint before search: ${profile.requiresCheckpointBeforeSearch ? "yes" : "no"}`,
2493
+ `- checkpoint before full text: ${profile.requiresCheckpointBeforeFullText ? "yes" : "no"}`
2494
+ ];
2495
+ if (profile.institutionalAccess) {
2496
+ lines.push(`- institutional access: ${profile.institutionalAccess.mode}; verified: no`);
2497
+ lines.push(` note: ${profile.institutionalAccess.note}`);
2498
+ }
2499
+ if (profile.publisherAccess) {
2500
+ lines.push(`- contact email: ${profile.publisherAccess.contactEmailPresent ? "present" : "missing"}`);
2501
+ lines.push(`- publisher adapters: ${profile.publisherAccess.records.length}`);
2502
+ }
2503
+ return lines.join("\n");
2504
+ }
2505
+ function renderAccessDoctor(profile, records, profilePath) {
2506
+ const metadataCapabilities = assessSearchSourceCapabilities([...SEARCH_SOURCES], env);
2507
+ const lines = [
2508
+ "LongTable Scholarly Access Doctor",
2509
+ `- readiness profile: ${profile ? "present" : "missing"} (${profilePath})`,
2510
+ `- metadata sources: ${metadataCapabilities.map((capability) => `${capability.source}:${capability.enabled ? "available" : "needs_config"}`).join(", ")}`,
2511
+ "- institutional access: LongTable cannot verify VPN/proxy/SSO until the researcher logs in.",
2512
+ "- secrets: LongTable stores env var names and capability status only, never credential values.",
2513
+ renderPublisherAccessRecords("Publisher API/TDM adapters", records)
2514
+ ];
2515
+ if (!profile) {
2516
+ lines.push("- next: run `longtable access setup` before PDF collection or full-text extraction.");
2517
+ }
2518
+ return lines.join("\n");
2519
+ }
2520
+ async function runAccessProbe(args) {
2402
2521
  if (typeof args.doi !== "string" || !args.doi.trim()) {
2403
- throw new Error("`longtable search probe` requires --doi <doi>.");
2522
+ throw new Error("`longtable access probe` requires --doi <doi>.");
2404
2523
  }
2405
2524
  const publisher = parsePublisherTarget(args.publisher);
2406
2525
  const record = await probePublisherAccess({
@@ -2416,7 +2535,7 @@ async function runSearchProbe(args) {
2416
2535
  }
2417
2536
  return [record];
2418
2537
  }
2419
- async function runSearchDoctor(args) {
2538
+ async function collectAccessDoctorRecords(args) {
2420
2539
  let records;
2421
2540
  if (typeof args.doi === "string" && args.doi.trim()) {
2422
2541
  if (args.publisher === "all") {
@@ -2434,104 +2553,148 @@ async function runSearchDoctor(args) {
2434
2553
  else {
2435
2554
  records = summarizeConfiguredPublisherAccess(env);
2436
2555
  }
2437
- const snapshotPath = searchCapabilitySnapshotPath();
2438
- const snapshotExists = existsSync(snapshotPath);
2556
+ return records;
2557
+ }
2558
+ async function runAccessDoctor(args) {
2559
+ const records = await collectAccessDoctorRecords(args);
2560
+ const profilePath = accessReadinessPath();
2561
+ const profile = readAccessReadinessProfile();
2439
2562
  if (args.json === true) {
2440
2563
  console.log(JSON.stringify({
2441
- capabilityFile: snapshotPath,
2442
- capabilityFileExists: snapshotExists,
2564
+ readinessFile: profilePath,
2565
+ readinessFileExists: Boolean(profile),
2566
+ readiness: profile,
2567
+ metadataSources: assessSearchSourceCapabilities([...SEARCH_SOURCES], env),
2443
2568
  records
2444
2569
  }, null, 2));
2445
2570
  }
2446
2571
  else {
2447
- console.log(renderPublisherAccessRecords("LongTable Search Publisher Access Doctor", records, snapshotPath));
2448
- if (!snapshotExists) {
2449
- console.log("- saved capabilities: none yet; run `longtable search setup` to record non-secret capability status.");
2450
- }
2572
+ console.log(renderAccessDoctor(profile, records, profilePath));
2451
2573
  }
2452
2574
  return records;
2453
2575
  }
2454
- async function promptPublisherDoi(rl, label, defaultDoi) {
2455
- const prompt = defaultDoi
2456
- ? `${label} test DOI [${defaultDoi}, Enter to reuse, skip to skip]: `
2457
- : `${label} test DOI (Enter to skip): `;
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;
2576
+ async function publisherRecordsForAccessSetup(args, routes) {
2577
+ const defaultDoi = typeof args.doi === "string" ? args.doi : undefined;
2578
+ if (!routes.includes("publisher_tdm")) {
2579
+ return summarizeConfiguredPublisherAccess(env);
2464
2580
  }
2465
- return answer;
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
- }
2581
+ if (defaultDoi) {
2582
+ return probeAllPublishers(defaultDoi);
2499
2583
  }
2500
- finally {
2501
- rl.close();
2584
+ if (input.isTTY && output.isTTY && args.json !== true) {
2585
+ const doi = await promptText("Optional DOI for publisher/TDM probing. Leave blank to record env-var readiness only.", false);
2586
+ return doi
2587
+ ? await probeAllPublishers(doi)
2588
+ : summarizeConfiguredPublisherAccess(env);
2502
2589
  }
2503
- return records;
2590
+ return summarizeConfiguredPublisherAccess(env);
2504
2591
  }
2505
- async function runSearchSetup(args) {
2506
- const defaultDoi = typeof args.doi === "string" ? args.doi : undefined;
2507
- const records = input.isTTY && output.isTTY && args.json !== true
2508
- ? await runInteractiveSearchSetup(defaultDoi)
2509
- : defaultDoi
2510
- ? await probeAllPublishers(defaultDoi)
2511
- : summarizeConfiguredPublisherAccess(env);
2512
- const snapshotPath = await saveSearchCapabilityRecords(records);
2592
+ async function runInteractiveAccessSetup(args) {
2593
+ const readiness = await promptChoice("Scholarly Access Readiness\n\nWill this machine/account use scholarly search or full-text access?", [
2594
+ { id: "configured", label: "Configure now", description: "Record access capability without storing secrets." },
2595
+ { id: "deferred", label: "Ask later", description: "Defer setup and require an access checkpoint before search or extraction." },
2596
+ { id: "disabled", label: "Do not use", description: "This project will use metadata/manual notes without scholarly full-text access." }
2597
+ ]);
2598
+ if (readiness !== "configured") {
2599
+ return buildAccessReadinessProfile({ readiness, routes: [] });
2600
+ }
2601
+ const routeSelections = await promptMultiChoice("Select every scholarly access route that is available or intended. Secrets are never stored.", [
2602
+ { id: "metadata", label: "Open metadata", description: "Crossref, OpenAlex, Semantic Scholar, PubMed, ERIC, DOAJ, Unpaywall." },
2603
+ { id: "oa_full_text", label: "OA full text", description: "Use open-access PDF/full-text when legally available." },
2604
+ { id: "institutional", label: "Institutional access", description: "VPN, library proxy, or browser SSO handled by the researcher." },
2605
+ { id: "publisher_tdm", label: "Publisher API/TDM", description: "Use configured publisher API/TDM environment variables." },
2606
+ { id: "manual_pdf", label: "Manual PDFs", description: "Researcher supplies PDFs; LongTable organizes/probes allowed extraction." },
2607
+ { id: "unknown", label: "Unknown", description: "Keep access uncertain and require a checkpoint before full-text work." }
2608
+ ]);
2609
+ const routes = uniqueAccessRoutes(routeSelections.length > 0 ? routeSelections : ["metadata"]);
2610
+ const institutionalAccessMode = routes.includes("institutional")
2611
+ ? await promptChoice("How will institutional access be completed? The researcher handles login/MFA directly.", [
2612
+ { id: "vpn", label: "VPN", description: "Researcher connects through school or institutional VPN." },
2613
+ { id: "library_proxy", label: "Library proxy", description: "Researcher uses proxy links or library resolver." },
2614
+ { id: "browser_sso", label: "Browser SSO", description: "Researcher logs into library/publisher SSO in the browser." },
2615
+ { id: "unknown", label: "Unknown", description: "The route exists but is not yet specified." }
2616
+ ])
2617
+ : undefined;
2618
+ const publisherRecords = await publisherRecordsForAccessSetup(args, routes);
2619
+ return buildAccessReadinessProfile({
2620
+ readiness,
2621
+ routes,
2622
+ institutionalAccessMode,
2623
+ publisherRecords
2624
+ });
2625
+ }
2626
+ async function runAccessSetup(args) {
2627
+ const records = typeof args.doi === "string" && args.doi.trim()
2628
+ ? await probeAllPublishers(args.doi)
2629
+ : summarizeConfiguredPublisherAccess(env);
2630
+ const profile = input.isTTY && output.isTTY && args.json !== true
2631
+ ? await runInteractiveAccessSetup(args)
2632
+ : buildAccessReadinessProfile({
2633
+ readiness: "configured",
2634
+ routes: inferAccessRoutes(records),
2635
+ publisherRecords: records
2636
+ });
2637
+ const profilePath = await saveAccessReadinessProfile(profile);
2513
2638
  if (args.json === true) {
2514
2639
  console.log(JSON.stringify({
2515
- capabilityFile: snapshotPath,
2516
- snapshot: buildSearchCapabilitySnapshot(records, env)
2640
+ readinessFile: profilePath,
2641
+ profile
2517
2642
  }, null, 2));
2518
2643
  return;
2519
2644
  }
2520
- console.log(renderPublisherAccessRecords("LongTable Search Publisher Access Setup", records, snapshotPath));
2645
+ console.log(renderAccessReadinessProfile(profile, profilePath));
2521
2646
  }
2522
- async function runSearch(subcommand, args) {
2523
- if (subcommand === "probe") {
2524
- await runSearchProbe(args);
2647
+ async function runAccessStatus(args) {
2648
+ const profilePath = accessReadinessPath();
2649
+ const profile = readAccessReadinessProfile();
2650
+ if (args.json === true) {
2651
+ console.log(JSON.stringify({
2652
+ readinessFile: profilePath,
2653
+ readinessFileExists: Boolean(profile),
2654
+ readiness: profile
2655
+ }, null, 2));
2525
2656
  return;
2526
2657
  }
2527
- if (subcommand === "doctor" || subcommand === "status") {
2528
- await runSearchDoctor(args);
2658
+ if (!profile) {
2659
+ console.log([
2660
+ "LongTable Scholarly Access Readiness",
2661
+ `- profile: missing (${profilePath})`,
2662
+ "- next: run `longtable access setup` before PDF collection or full-text extraction."
2663
+ ].join("\n"));
2529
2664
  return;
2530
2665
  }
2666
+ console.log(renderAccessReadinessProfile(profile, profilePath));
2667
+ }
2668
+ async function runAccess(subcommand, args) {
2531
2669
  if (subcommand === "setup") {
2532
- await runSearchSetup(args);
2670
+ await runAccessSetup(args);
2671
+ return;
2672
+ }
2673
+ if (subcommand === "status") {
2674
+ await runAccessStatus(args);
2675
+ return;
2676
+ }
2677
+ if (subcommand === "doctor") {
2678
+ await runAccessDoctor(args);
2533
2679
  return;
2534
2680
  }
2681
+ if (subcommand === "probe") {
2682
+ await runAccessProbe(args);
2683
+ return;
2684
+ }
2685
+ if (!subcommand) {
2686
+ await runAccessStatus(args);
2687
+ return;
2688
+ }
2689
+ throw new Error(`Unknown access subcommand: ${subcommand}`);
2690
+ }
2691
+ function movedSearchAccessCommand(subcommand) {
2692
+ return new Error(`\`longtable search ${subcommand}\` has moved. Use \`longtable access ${subcommand}\`.`);
2693
+ }
2694
+ async function runSearch(subcommand, args) {
2695
+ if (subcommand === "setup" || subcommand === "doctor" || subcommand === "status" || subcommand === "probe") {
2696
+ throw movedSearchAccessCommand(subcommand);
2697
+ }
2535
2698
  if (subcommand) {
2536
2699
  throw new Error(`Unknown search subcommand: ${subcommand}`);
2537
2700
  }
@@ -3524,6 +3687,10 @@ async function main() {
3524
3687
  await runMcpSubcommand(subcommand, values);
3525
3688
  return;
3526
3689
  }
3690
+ if (command === "access") {
3691
+ await runAccess(subcommand, values);
3692
+ return;
3693
+ }
3527
3694
  if (command === "search") {
3528
3695
  await runSearch(subcommand, values);
3529
3696
  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) && looksLikeResearchCommitmentPrompt(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
- shouldCreateRequiredQuestionsForPrompt(prompt) &&
197
+ looksLikeResearchCommitmentPrompt(prompt) &&
192
198
  !looksLikeQuestionGenerationPrompt(prompt) &&
193
199
  !looksLikeMultiCommitmentChangePrompt(prompt);
194
200
  }
@@ -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
  }
@@ -1219,8 +1228,8 @@ function questionPriority(spec) {
1219
1228
  const requiredWeight = spec.kind === "research_commitment" || spec.required ? 20 : 0;
1220
1229
  return (byKey[spec.key] ?? 0) + confidenceWeight + requiredWeight;
1221
1230
  }
1222
- function followUpQuestionOptions(first, second, third, fourth) {
1223
- return [first, second, third, ...(fourth ? [fourth] : [])];
1231
+ function followUpQuestionOptions(first, second, third, ...rest) {
1232
+ return [first, second, third, ...rest];
1224
1233
  }
1225
1234
  export function buildQuestionOpportunitySpecs(prompt, options = {}) {
1226
1235
  const normalized = prompt.toLowerCase();
@@ -1285,8 +1294,26 @@ export function buildQuestionOpportunitySpecs(prompt, options = {}) {
1285
1294
  /\brandom[- ]?effects\b/i,
1286
1295
  /분석\s*계획|분석\s*방법|메타\s*분석|분석\s*(?:모형|모델)|통계\s*(?:모형|모델)|구조\s*방정식|경로\s*모형|조절효과|랜덤\s*효과/
1287
1296
  ]);
1297
+ const accessCue = includesAny(normalized, [
1298
+ /\b(pdf|full[- ]?text|tdm|publisher api|institutional access|library login|vpn|proxy|subscription|paper collection|source collection|corpus|download)\b/i,
1299
+ /PDF|원문|전문|기관\s*구독|기관구독|구독|VPN|프록시|도서관|라이브러리|TDM|논문\s*수집|문헌\s*수집|코퍼스|다운로드/
1300
+ ]);
1288
1301
  const decisionFamilyCount = [scopeCue, theoryCue, measurementCodingCue, methodCue, analysisCue]
1289
1302
  .filter(Boolean).length;
1303
+ if (accessCue) {
1304
+ push({
1305
+ key: "scholarly_access_policy",
1306
+ kind: "evidence_risk",
1307
+ title: "Scholarly access policy",
1308
+ question: "What scholarly access route should LongTable use before collecting PDFs, full text, or subscription-only evidence?",
1309
+ whyNow: "Full-text access decisions can change the corpus, inclusion bias, reproducibility, and TDM permission boundary.",
1310
+ 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." }),
1311
+ confidence: "high",
1312
+ autoEligible: true,
1313
+ required: true,
1314
+ cues: ["scholarly_access", "full_text", "corpus"]
1315
+ });
1316
+ }
1290
1317
  if (decisionActionCue && decisionFamilyCount >= 2) {
1291
1318
  push({
1292
1319
  key: "research_direction_change_commitment",
@@ -1619,7 +1646,7 @@ export function buildQuestionOpportunitySpecs(prompt, options = {}) {
1619
1646
  }
1620
1647
  let selected = options.autoOnly === true ? specs.filter((spec) => spec.autoEligible) : specs;
1621
1648
  if (options.requiredOnly === true) {
1622
- selected = selected.filter((spec) => spec.kind === "research_commitment");
1649
+ selected = selected.filter((spec) => spec.kind === "research_commitment" || spec.required);
1623
1650
  }
1624
1651
  if (normalized.includes("protected decision closure pressure")) {
1625
1652
  selected = selected.filter((spec) => spec.key === "protected_decision_closure");
@@ -1,4 +1,4 @@
1
- import { type CrossrefTdmDiscovery, type EvidenceCard, type Publisher, type PublisherAccessRecord, type PublisherProbeInput, type PublisherProbeTarget, type SearchCapabilitySnapshot, type SearchFetch } from "./types.js";
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";
@@ -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.44",
3
+ "version": "0.1.45",
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.44",
33
- "@longtable/core": "0.1.44",
34
- "@longtable/memory": "0.1.44",
35
- "@longtable/provider-claude": "0.1.44",
36
- "@longtable/provider-codex": "0.1.44",
37
- "@longtable/setup": "0.1.44"
32
+ "@longtable/checkpoints": "0.1.45",
33
+ "@longtable/core": "0.1.45",
34
+ "@longtable/memory": "0.1.45",
35
+ "@longtable/provider-claude": "0.1.45",
36
+ "@longtable/provider-codex": "0.1.45",
37
+ "@longtable/setup": "0.1.45"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@types/node": "^22.10.1",