@longtable/cli 0.1.47 → 0.1.48

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -18,7 +18,7 @@ import { buildPersonaGuidance, parseInvocationDirective } from "./persona-router
18
18
  import { PERSONA_DEFINITIONS, listRoleDefinitions } from "./personas.js";
19
19
  import { buildPanelFallback, renderPanelSummary } from "./panel.js";
20
20
  import { LONGTABLE_MANAGED_HOOK_EVENTS, codexHooksEnabled, enableCodexHooksFeature, getMissingManagedCodexHookEvents, mergeManagedCodexHooksConfig, removeManagedCodexHooks } from "./codex-hooks.js";
21
- import { appendInvocationRecordToWorkspace, assertWorkspaceNotBlocked, answerWorkspaceQuestion, buildQuestionOpportunitySpecs, clearWorkspaceQuestion, createWorkspaceFollowUpQuestions, createWorkspaceQuestion, createOrUpdateProjectWorkspace, inspectProjectWorkspace, loadWorkspaceState, loadProjectContextFromDirectory, pruneWorkspaceQuestions, repairWorkspaceStateConsistency, renderProjectWorkspaceSummary, syncCurrentWorkspaceView } from "./project-session.js";
21
+ import { appendInvocationRecordToWorkspace, applyResearchSpecificationPatch, assertWorkspaceNotBlocked, answerWorkspaceQuestion, buildQuestionOpportunitySpecs, clearWorkspaceQuestion, createWorkspaceFollowUpQuestions, createWorkspaceQuestion, createOrUpdateProjectWorkspace, diffResearchSpecifications, inspectProjectWorkspace, loadWorkspaceState, loadProjectContextFromDirectory, findUnincorporatedResearchEvidence, proposeResearchSpecificationPatch, pruneWorkspaceQuestions, readResearchSpecificationHistory, repairWorkspaceStateConsistency, renderProjectWorkspaceSummary, syncCurrentWorkspaceView } from "./project-session.js";
22
22
  import { buildTeamDebate, buildTeamReview, renderTeamDebateSummary } from "./debate.js";
23
23
  import { createPromptRenderer } from "./prompt-renderer.js";
24
24
  const VALID_MODES = new Set([
@@ -62,6 +62,11 @@ const LONGTABLE_MCP_MANAGED_TOOLS = [
62
62
  "summarize_interview",
63
63
  "summarize_research_specification",
64
64
  "read_research_specification",
65
+ "propose_research_spec_patch",
66
+ "apply_research_spec_patch",
67
+ "diff_research_specification",
68
+ "read_research_spec_history",
69
+ "find_unincorporated_evidence",
65
70
  "cancel_interview",
66
71
  "confirm_first_research_shape",
67
72
  "confirm_research_specification",
@@ -76,6 +81,11 @@ const LONGTABLE_MCP_MANAGED_TOOLS = [
76
81
  const LONGTABLE_MCP_RESEARCH_SPECIFICATION_TOOLS = [
77
82
  "summarize_research_specification",
78
83
  "read_research_specification",
84
+ "propose_research_spec_patch",
85
+ "apply_research_spec_patch",
86
+ "diff_research_specification",
87
+ "read_research_spec_history",
88
+ "find_unincorporated_evidence",
79
89
  "confirm_research_specification"
80
90
  ];
81
91
  function style(text, prefix) {
@@ -132,6 +142,7 @@ function usage() {
132
142
  " longtable doctor [--cwd <path>] [--fix] [--json] [--codex-dir <path>] [--codex-config <path>] [--hooks-path <path>] [--claude-dir <path>] [--codex-prompts-dir <path>] [--codex-runtime-path <file>] [--claude-runtime-path <file>]",
133
143
  " longtable status [--cwd <path>] [--fix] [--json] [--codex-dir <path>] [--codex-config <path>] [--hooks-path <path>] [--claude-dir <path>] [--codex-prompts-dir <path>] [--codex-runtime-path <file>] [--claude-runtime-path <file>]",
134
144
  " longtable audit [questions|roles] [--json]",
145
+ " longtable spec [read|history|diff|unincorporated|apply|propose] [--cwd <path>] [--json] [--spec-file <path>] [--patch-id <id>]",
135
146
  " longtable roles [--json]",
136
147
  " longtable show [--json] [--path <file>]",
137
148
  " longtable install [--json] [--path <file>] [--runtime-path <file>]",
@@ -182,7 +193,7 @@ function parseArgs(argv) {
182
193
  const values = {};
183
194
  let subcommand = maybeSubcommand;
184
195
  const modeCommand = command && VALID_MODES.has(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);
196
+ 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", "spec"].includes(command);
186
197
  let startIndex = 1;
187
198
  if (modeCommand) {
188
199
  subcommand = undefined;
@@ -191,7 +202,7 @@ function parseArgs(argv) {
191
202
  else if (command === "codex" || command === "claude" || command === "mcp") {
192
203
  startIndex = 2;
193
204
  }
194
- else if ((command === "access" || command === "search") && maybeSubcommand && !maybeSubcommand.startsWith("--")) {
205
+ else if ((command === "access" || command === "search" || command === "spec") && maybeSubcommand && !maybeSubcommand.startsWith("--")) {
195
206
  subcommand = maybeSubcommand;
196
207
  startIndex = 2;
197
208
  }
@@ -2851,6 +2862,108 @@ async function runSearch(subcommand, args) {
2851
2862
  }
2852
2863
  console.log(renderEvidenceRunSummary(run, recordedPath));
2853
2864
  }
2865
+ async function requireWorkspaceContext(args) {
2866
+ const workingDirectory = typeof args.cwd === "string" ? args.cwd : cwd();
2867
+ const context = await loadProjectContextFromDirectory(workingDirectory);
2868
+ if (!context) {
2869
+ throw new Error("No LongTable workspace was found from the supplied cwd.");
2870
+ }
2871
+ return context;
2872
+ }
2873
+ async function readResearchSpecificationFile(path) {
2874
+ if (!path) {
2875
+ throw new Error("A Research Specification JSON file is required. Use --spec-file <path>.");
2876
+ }
2877
+ return JSON.parse(await readFile(resolve(path), "utf8"));
2878
+ }
2879
+ async function runSpec(subcommand, args) {
2880
+ const context = await requireWorkspaceContext(args);
2881
+ const command = subcommand ?? "read";
2882
+ if (command === "read" || command === "history") {
2883
+ const history = await readResearchSpecificationHistory(context);
2884
+ if (args.json === true) {
2885
+ console.log(JSON.stringify(history, null, 2));
2886
+ return;
2887
+ }
2888
+ console.log("LongTable Research Specification");
2889
+ console.log(`- title: ${history.specification?.title ?? "missing"}`);
2890
+ console.log(`- status: ${history.specification?.confirmedAt ? "confirmed" : history.specification?.status ?? "missing"}`);
2891
+ console.log(`- revisions: ${history.revisions.length}`);
2892
+ console.log(`- patches: ${history.patches.length}`);
2893
+ console.log(`- evidence records: ${history.evidenceRecords.length}`);
2894
+ for (const revision of history.revisions.slice(-5).reverse()) {
2895
+ console.log(`- v${revision.index}: ${revision.title} (${revision.changeSummary.slice(0, 2).join("; ")})`);
2896
+ }
2897
+ return;
2898
+ }
2899
+ if (command === "unincorporated") {
2900
+ const evidenceRecords = await findUnincorporatedResearchEvidence(context);
2901
+ if (args.json === true) {
2902
+ console.log(JSON.stringify({ evidenceRecords }, null, 2));
2903
+ return;
2904
+ }
2905
+ console.log("Unincorporated Research Evidence");
2906
+ for (const record of evidenceRecords.slice(-10).reverse()) {
2907
+ console.log(`- ${record.id} [${record.sourceKind}]: ${record.summary}`);
2908
+ }
2909
+ return;
2910
+ }
2911
+ if (command === "diff") {
2912
+ const specification = await readResearchSpecificationFile(typeof args["spec-file"] === "string" ? args["spec-file"] : undefined);
2913
+ const state = await loadWorkspaceState(context);
2914
+ const changes = diffResearchSpecifications(state.researchSpecification, specification);
2915
+ if (args.json === true) {
2916
+ console.log(JSON.stringify({ changes }, null, 2));
2917
+ return;
2918
+ }
2919
+ console.log("Research Specification Diff");
2920
+ for (const change of changes) {
2921
+ console.log(`- ${change.summary}`);
2922
+ }
2923
+ return;
2924
+ }
2925
+ if (command === "propose") {
2926
+ const specification = await readResearchSpecificationFile(typeof args["spec-file"] === "string" ? args["spec-file"] : undefined);
2927
+ const result = await proposeResearchSpecificationPatch({
2928
+ context,
2929
+ specification,
2930
+ source: "manual",
2931
+ rationale: typeof args.rationale === "string" ? args.rationale : undefined
2932
+ });
2933
+ if (args.json === true) {
2934
+ console.log(JSON.stringify(result, null, 2));
2935
+ return;
2936
+ }
2937
+ console.log("Research Specification patch proposed");
2938
+ console.log(`- patch: ${result.patch.id}`);
2939
+ console.log(`- changes: ${result.changes.length}`);
2940
+ console.log(`- apply: longtable spec apply --patch-id ${result.patch.id}`);
2941
+ return;
2942
+ }
2943
+ if (command === "apply") {
2944
+ const specification = typeof args["spec-file"] === "string"
2945
+ ? await readResearchSpecificationFile(args["spec-file"])
2946
+ : undefined;
2947
+ const result = await applyResearchSpecificationPatch({
2948
+ context,
2949
+ patchId: typeof args["patch-id"] === "string" ? args["patch-id"] : undefined,
2950
+ specification,
2951
+ source: "manual",
2952
+ rationale: typeof args.rationale === "string" ? args.rationale : undefined
2953
+ });
2954
+ if (args.json === true) {
2955
+ console.log(JSON.stringify(result, null, 2));
2956
+ return;
2957
+ }
2958
+ console.log("Research Specification patch applied");
2959
+ console.log(`- revision: v${result.revision.index} (${result.revision.id})`);
2960
+ console.log(`- patch: ${result.patch.id}`);
2961
+ console.log(`- decision: ${result.decision?.id ?? result.patch.decisionRecordId ?? "existing/none"}`);
2962
+ console.log(`- current: ${context.currentFilePath}`);
2963
+ return;
2964
+ }
2965
+ throw new Error(`Unknown spec subcommand: ${command}`);
2966
+ }
2854
2967
  async function runQuestion(args) {
2855
2968
  const workingDirectory = typeof args.cwd === "string" ? args.cwd : cwd();
2856
2969
  const prompt = await resolvePrompt(typeof args.prompt === "string" ? args.prompt : undefined);
@@ -3807,6 +3920,10 @@ async function main() {
3807
3920
  await runSearch(subcommand, values);
3808
3921
  return;
3809
3922
  }
3923
+ if (command === "spec") {
3924
+ await runSpec(subcommand, values);
3925
+ return;
3926
+ }
3810
3927
  if (command === "ask") {
3811
3928
  await runAsk(values);
3812
3929
  return;
@@ -1,4 +1,4 @@
1
- import type { DecisionRecord, InvocationRecord, LongTableQuestionObligation, ProviderKind, QuestionOption, QuestionCommitmentFamily, QuestionEpistemicBasis, QuestionGenerationResult, QuestionOpportunity, QuestionSurface, QuestionRecord, ResearchState } from "@longtable/core";
1
+ import type { DecisionRecord, EvidenceRecord, InvocationRecord, LongTableQuestionObligation, ProviderKind, QuestionOption, QuestionCommitmentFamily, QuestionEpistemicBasis, QuestionGenerationResult, QuestionOpportunity, QuestionSurface, QuestionRecord, ResearchSpecificationChange, ResearchSpecificationPatch, ResearchSpecificationPatchSource, ResearchSpecificationRevision, 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";
@@ -40,6 +40,9 @@ export interface ResearchSpecification {
40
40
  createdAt?: string;
41
41
  updatedAt?: string;
42
42
  sourceHookId?: string;
43
+ latestRevisionId?: string;
44
+ sourceEvidenceIds?: string[];
45
+ sectionEvidence?: Record<string, string[]>;
43
46
  researchDirection: {
44
47
  question?: string;
45
48
  purpose: string;
@@ -121,6 +124,10 @@ export type LongTableWorkspaceState = ResearchState & {
121
124
  hooks?: LongTableHookRun[];
122
125
  firstResearchShape?: FirstResearchShape;
123
126
  researchSpecification?: ResearchSpecification;
127
+ interviewTurns?: LongTableInterviewTurn[];
128
+ evidenceRecords?: EvidenceRecord[];
129
+ specPatches?: ResearchSpecificationPatch[];
130
+ specRevisions?: ResearchSpecificationRevision[];
124
131
  };
125
132
  export interface LongTableProjectRecord {
126
133
  schemaVersion: 1;
@@ -206,6 +213,10 @@ export interface LongTableWorkspaceInspection {
206
213
  pendingObligations: number;
207
214
  answeredQuestions: number;
208
215
  decisions: number;
216
+ interviewTurns?: number;
217
+ evidenceRecords?: number;
218
+ specPatches?: number;
219
+ specRevisions?: number;
209
220
  };
210
221
  recentInvocations?: Array<{
211
222
  id: string;
@@ -250,6 +261,25 @@ export interface LongTableWorkspaceInspection {
250
261
  }>;
251
262
  }
252
263
  export declare function loadWorkspaceState(context: LongTableProjectContext): Promise<LongTableWorkspaceState>;
264
+ export declare function diffResearchSpecifications(before: ResearchSpecification | undefined, after: ResearchSpecification): ResearchSpecificationChange[];
265
+ export declare function applyResearchSpecificationAuditUpdate(state: LongTableWorkspaceState, options: {
266
+ specification: ResearchSpecification;
267
+ timestamp: string;
268
+ source: ResearchSpecificationPatchSource;
269
+ title?: string;
270
+ rationale?: string;
271
+ sourceEvidenceIds?: string[];
272
+ patch?: ResearchSpecificationPatch;
273
+ questionRecordId?: string;
274
+ decisionRecordId?: string;
275
+ createDecisionRecord?: boolean;
276
+ }): {
277
+ state: LongTableWorkspaceState;
278
+ specification: ResearchSpecification;
279
+ patch: ResearchSpecificationPatch;
280
+ revision: ResearchSpecificationRevision;
281
+ decision?: DecisionRecord;
282
+ };
253
283
  export declare function syncCurrentWorkspaceView(context: LongTableProjectContext): Promise<string>;
254
284
  export declare function appendInvocationRecordToWorkspace(context: LongTableProjectContext, invocation: InvocationRecord, questions?: QuestionRecord[]): Promise<LongTableWorkspaceState>;
255
285
  export declare function beginLongTableInterview(options: {
@@ -298,6 +328,41 @@ export declare function summarizeLongTableResearchSpecification(options: {
298
328
  state: LongTableWorkspaceState;
299
329
  session: LongTableSessionRecord;
300
330
  }>;
331
+ export declare function proposeResearchSpecificationPatch(options: {
332
+ context: LongTableProjectContext;
333
+ specification: ResearchSpecification;
334
+ source?: ResearchSpecificationPatchSource;
335
+ rationale?: string;
336
+ sourceEvidenceIds?: string[];
337
+ }): Promise<{
338
+ patch: ResearchSpecificationPatch;
339
+ changes: ResearchSpecificationChange[];
340
+ state: LongTableWorkspaceState;
341
+ }>;
342
+ export declare function applyResearchSpecificationPatch(options: {
343
+ context: LongTableProjectContext;
344
+ patchId?: string;
345
+ specification?: ResearchSpecification;
346
+ source?: ResearchSpecificationPatchSource;
347
+ rationale?: string;
348
+ sourceEvidenceIds?: string[];
349
+ questionRecordId?: string;
350
+ decisionRecordId?: string;
351
+ }): Promise<{
352
+ patch: ResearchSpecificationPatch;
353
+ revision: ResearchSpecificationRevision;
354
+ specification: ResearchSpecification;
355
+ state: LongTableWorkspaceState;
356
+ session: LongTableSessionRecord;
357
+ decision?: DecisionRecord;
358
+ }>;
359
+ export declare function readResearchSpecificationHistory(context: LongTableProjectContext): Promise<{
360
+ specification?: ResearchSpecification;
361
+ revisions: ResearchSpecificationRevision[];
362
+ patches: ResearchSpecificationPatch[];
363
+ evidenceRecords: EvidenceRecord[];
364
+ }>;
365
+ export declare function findUnincorporatedResearchEvidence(context: LongTableProjectContext): Promise<EvidenceRecord[]>;
301
366
  export declare function listBlockingWorkspaceQuestions(context: LongTableProjectContext): Promise<QuestionRecord[]>;
302
367
  export declare function listBlockingWorkspaceObligations(context: LongTableProjectContext): Promise<LongTableQuestionObligation[]>;
303
368
  export declare function assertWorkspaceNotBlocked(context: LongTableProjectContext): Promise<void>;
@@ -200,7 +200,66 @@ function renderResearchSpecificationStatus(session, locale) {
200
200
  : "- Next protocol: update the specification, then return to `confirm_research_specification` for another preview confirmation."
201
201
  ];
202
202
  }
203
- function buildCurrentGuide(project, session, recentInvocations = [], pendingQuestions = [], pendingObligations = []) {
203
+ function renderResearchSpecificationAudit(state, locale) {
204
+ const korean = locale === "ko";
205
+ const revisions = (state.specRevisions ?? []).slice(-5).reverse();
206
+ const patches = (state.specPatches ?? []).slice(-5).reverse();
207
+ const evidenceRecords = state.evidenceRecords ?? [];
208
+ const unincorporated = evidenceRecords
209
+ .filter((record) => !record.incorporatedByRevisionId)
210
+ .slice(-5)
211
+ .reverse();
212
+ const specification = state.researchSpecification;
213
+ const sectionEvidence = Object.entries(specification?.sectionEvidence ?? {}).slice(0, 8);
214
+ if (revisions.length === 0 &&
215
+ patches.length === 0 &&
216
+ evidenceRecords.length === 0 &&
217
+ sectionEvidence.length === 0) {
218
+ return [];
219
+ }
220
+ return [
221
+ "",
222
+ korean ? "## Research Specification 감사" : "## Research Specification Audit",
223
+ ...(specification
224
+ ? [
225
+ `- ${korean ? "현재 버전" : "Current revision"}: ${specification.latestRevisionId ?? "unversioned"}`,
226
+ `- ${korean ? "상태" : "Status"}: ${specification.confirmedAt ? "confirmed" : specification.status ?? "draft"}`
227
+ ]
228
+ : []),
229
+ `- ${korean ? "원문 인터뷰 turn" : "Raw interview turns"}: ${(state.interviewTurns ?? []).length}`,
230
+ `- ${korean ? "근거 기록" : "Evidence records"}: ${evidenceRecords.length}`,
231
+ `- ${korean ? "spec patch" : "Spec patches"}: ${(state.specPatches ?? []).length}`,
232
+ `- ${korean ? "spec revision" : "Spec revisions"}: ${(state.specRevisions ?? []).length}`,
233
+ ...(revisions.length > 0
234
+ ? [
235
+ "",
236
+ korean ? "### 최근 명세 변경" : "### Recent Specification Changes",
237
+ ...revisions.map((revision) => `- v${revision.index} ${revision.title}: ${revision.changeSummary.slice(0, 3).join("; ")}`)
238
+ ]
239
+ : []),
240
+ ...(sectionEvidence.length > 0
241
+ ? [
242
+ "",
243
+ korean ? "### 근거 맵" : "### Evidence Map",
244
+ ...sectionEvidence.map(([path, ids]) => `- ${path}: ${ids.slice(-4).join(", ")}`)
245
+ ]
246
+ : []),
247
+ ...(unincorporated.length > 0
248
+ ? [
249
+ "",
250
+ korean ? "### 아직 반영되지 않은 근거" : "### Unincorporated Evidence",
251
+ ...unincorporated.map((record) => `- ${record.id} [${record.sourceKind}]: ${compactLine(record.summary, 120)}`)
252
+ ]
253
+ : []),
254
+ ...(patches.some((patch) => patch.status === "proposed")
255
+ ? [
256
+ "",
257
+ korean ? "- 대기 중인 spec patch가 있습니다. 적용하거나 거절해야 합니다." : "- Proposed spec patches are waiting to be applied or rejected."
258
+ ]
259
+ : [])
260
+ ];
261
+ }
262
+ function buildCurrentGuide(project, session, recentInvocations = [], pendingQuestions = [], pendingObligations = [], state = createEmptyResearchState()) {
204
263
  const locale = normalizeLocale(session.locale ?? project.locale);
205
264
  const openQuestions = session.openQuestions && session.openQuestions.length > 0
206
265
  ? session.openQuestions
@@ -229,6 +288,7 @@ function buildCurrentGuide(project, session, recentInvocations = [], pendingQues
229
288
  `- 관점: ${session.requestedPerspectives.length > 0 ? session.requestedPerspectives.join(", ") : "auto"}`,
230
289
  `- disagreement: ${session.disagreementPreference}`,
231
290
  ...renderResearchSpecificationStatus(session, locale),
291
+ ...renderResearchSpecificationAudit(state, locale),
232
292
  "",
233
293
  "## 열린 질문",
234
294
  ...openQuestions.map((question) => `- ${question}`),
@@ -305,6 +365,7 @@ function buildCurrentGuide(project, session, recentInvocations = [], pendingQues
305
365
  `- Perspectives: ${session.requestedPerspectives.length > 0 ? session.requestedPerspectives.join(", ") : "auto"}`,
306
366
  `- Disagreement: ${session.disagreementPreference}`,
307
367
  ...renderResearchSpecificationStatus(session, locale),
368
+ ...renderResearchSpecificationAudit(state, locale),
308
369
  "",
309
370
  "## Open Questions",
310
371
  ...openQuestions.map((question) => `- ${question}`),
@@ -373,6 +434,10 @@ async function loadResearchState(stateFilePath) {
373
434
  hooks: parsed.hooks ?? [],
374
435
  ...(parsed.firstResearchShape ? { firstResearchShape: parsed.firstResearchShape } : {}),
375
436
  ...(parsed.researchSpecification ? { researchSpecification: parsed.researchSpecification } : {}),
437
+ interviewTurns: parsed.interviewTurns ?? [],
438
+ evidenceRecords: parsed.evidenceRecords ?? [],
439
+ specPatches: parsed.specPatches ?? [],
440
+ specRevisions: parsed.specRevisions ?? [],
376
441
  questionObligations: parsed.questionObligations ?? [],
377
442
  inferredHypotheses: parsed.inferredHypotheses ?? [],
378
443
  openTensions: parsed.openTensions ?? [],
@@ -421,6 +486,280 @@ function formatQuestionMetadata(record) {
421
486
  ].filter(Boolean);
422
487
  return parts.length > 0 ? ` [${parts.join("; ")}]` : "";
423
488
  }
489
+ function compactLine(value, limit = 160) {
490
+ const compacted = value.replace(/\s+/g, " ").trim();
491
+ return compacted.length > limit ? `${compacted.slice(0, limit - 1)}…` : compacted;
492
+ }
493
+ function asRecord(value) {
494
+ return value !== null && typeof value === "object" && !Array.isArray(value)
495
+ ? value
496
+ : null;
497
+ }
498
+ const SPEC_DIFF_IGNORED_PATHS = new Set([
499
+ "createdAt",
500
+ "updatedAt",
501
+ "latestRevisionId",
502
+ "sourceEvidenceIds",
503
+ "sectionEvidence"
504
+ ]);
505
+ function flattenSpecificationValue(value, prefix = "") {
506
+ const flattened = new Map();
507
+ const record = asRecord(value);
508
+ if (!record) {
509
+ if (prefix) {
510
+ flattened.set(prefix, value);
511
+ }
512
+ return flattened;
513
+ }
514
+ for (const [key, nested] of Object.entries(record)) {
515
+ const path = prefix ? `${prefix}.${key}` : key;
516
+ if (SPEC_DIFF_IGNORED_PATHS.has(path)) {
517
+ continue;
518
+ }
519
+ const nestedRecord = asRecord(nested);
520
+ if (nestedRecord) {
521
+ for (const [nestedPath, nestedValue] of flattenSpecificationValue(nestedRecord, path)) {
522
+ flattened.set(nestedPath, nestedValue);
523
+ }
524
+ continue;
525
+ }
526
+ flattened.set(path, nested);
527
+ }
528
+ return flattened;
529
+ }
530
+ function stableValue(value) {
531
+ return JSON.stringify(value ?? null);
532
+ }
533
+ export function diffResearchSpecifications(before, after) {
534
+ const beforeMap = before ? flattenSpecificationValue(before) : new Map();
535
+ const afterMap = flattenSpecificationValue(after);
536
+ const paths = new Set([...beforeMap.keys(), ...afterMap.keys()]);
537
+ const changes = [];
538
+ for (const path of [...paths].sort()) {
539
+ const previous = beforeMap.get(path);
540
+ const next = afterMap.get(path);
541
+ if (stableValue(previous) === stableValue(next)) {
542
+ continue;
543
+ }
544
+ const kind = previous === undefined
545
+ ? "set"
546
+ : next === undefined
547
+ ? "remove"
548
+ : "replace";
549
+ changes.push({
550
+ path,
551
+ kind,
552
+ summary: `${kind} ${path}`,
553
+ ...(previous !== undefined ? { before: previous } : {}),
554
+ ...(next !== undefined ? { after: next } : {})
555
+ });
556
+ }
557
+ return changes;
558
+ }
559
+ function cloneResearchSpecification(specification) {
560
+ return JSON.parse(JSON.stringify(specification));
561
+ }
562
+ function mergeStringLists(...lists) {
563
+ return [...new Set(lists.flatMap((list) => list ?? []).filter(Boolean))];
564
+ }
565
+ function requiredResearchSpecificationGaps(specification) {
566
+ const gaps = [];
567
+ if (!specification.researchDirection.question?.trim()) {
568
+ gaps.push("research question");
569
+ }
570
+ if (specification.constructOntology.coreConstructs.length === 0) {
571
+ gaps.push("construct map/core constructs");
572
+ }
573
+ if (specification.researchDirection.inclusionCriteria?.length === 0 &&
574
+ specification.researchDirection.exclusionCriteria?.length === 0) {
575
+ gaps.push("inclusion/exclusion rule");
576
+ }
577
+ if (specification.evidenceAccess.requiredSources?.length === 0 &&
578
+ specification.evidenceAccess.evidenceStandards?.length === 0) {
579
+ gaps.push("evidence boundary");
580
+ }
581
+ if (!specification.methodAnalysis.design?.trim() &&
582
+ specification.methodAnalysis.analysisOptions.length === 0) {
583
+ gaps.push("method commitment");
584
+ }
585
+ if (specification.openQuestions.length === 0 && specification.protectedDecisions.length === 0) {
586
+ gaps.push("unresolved decisions/protected decisions");
587
+ }
588
+ if (specification.evidenceAccess.accessRequirements?.length === 0 &&
589
+ specification.evidenceAccess.requiredSources?.length === 0) {
590
+ gaps.push("search/access assumptions");
591
+ }
592
+ return gaps;
593
+ }
594
+ function buildResearchSpecificationGapQuestion(gaps, timestamp, sourceEvidenceIds) {
595
+ return {
596
+ id: createId("question"),
597
+ createdAt: timestamp,
598
+ updatedAt: timestamp,
599
+ status: "pending",
600
+ commitmentFamily: "scope",
601
+ epistemicBasis: "project_state",
602
+ prompt: {
603
+ id: createId("prompt"),
604
+ checkpointKey: "research_specification_required_sections",
605
+ title: "Research Specification gaps",
606
+ question: `Which missing Research Specification section should LongTable resolve first? Missing: ${gaps.join(", ")}.`,
607
+ type: "single_choice",
608
+ options: [
609
+ { value: "ask_researcher", label: "Ask the researcher", description: "Pause and ask for the missing research commitment.", recommended: true },
610
+ { value: "mark_unresolved", label: "Mark unresolved", description: "Keep the gap visible as an unresolved decision." },
611
+ { value: "infer_from_evidence", label: "Infer from evidence", description: "Use existing evidence records and keep the inference explicit." },
612
+ { value: "defer", label: "Defer", description: "Do not treat the specification as complete yet." }
613
+ ],
614
+ allowOther: true,
615
+ otherLabel: "Other resolution",
616
+ required: true,
617
+ source: "checkpoint",
618
+ displayReason: `The current Research Specification is missing: ${gaps.join(", ")}.`,
619
+ rationale: [
620
+ "Research Specification is the required durable interview artifact.",
621
+ "Missing required sections can make later resume, screening, coding, or evidence decisions stale."
622
+ ],
623
+ preferredSurfaces: ["mcp_elicitation", "numbered"]
624
+ },
625
+ transportStatus: {
626
+ surface: "mcp_elicitation",
627
+ status: "not_attempted",
628
+ updatedAt: timestamp,
629
+ ...(sourceEvidenceIds.length > 0 ? { message: `Source evidence: ${sourceEvidenceIds.join(", ")}` } : {})
630
+ }
631
+ };
632
+ }
633
+ function appendSpecGapQuestionIfNeeded(state, specification, timestamp, sourceEvidenceIds) {
634
+ const gaps = requiredResearchSpecificationGaps(specification);
635
+ if (gaps.length === 0) {
636
+ return state;
637
+ }
638
+ const alreadyPending = (state.questionLog ?? []).some((record) => record.status === "pending" &&
639
+ record.prompt.checkpointKey === "research_specification_required_sections");
640
+ if (alreadyPending) {
641
+ return state;
642
+ }
643
+ return {
644
+ ...state,
645
+ questionLog: [
646
+ ...(state.questionLog ?? []),
647
+ buildResearchSpecificationGapQuestion(gaps, timestamp, sourceEvidenceIds)
648
+ ]
649
+ };
650
+ }
651
+ function changeSummaryForRevision(changes) {
652
+ if (changes.length === 0) {
653
+ return ["No substantive field changes; audit metadata refreshed."];
654
+ }
655
+ return changes.slice(0, 12).map((change) => change.summary);
656
+ }
657
+ export function applyResearchSpecificationAuditUpdate(state, options) {
658
+ const previous = state.researchSpecification;
659
+ const incomingEvidenceIds = mergeStringLists(options.patch?.sourceEvidenceIds, options.specification.sourceEvidenceIds, options.sourceEvidenceIds);
660
+ const sourceEvidenceIds = mergeStringLists(previous?.sourceEvidenceIds, incomingEvidenceIds);
661
+ const changes = diffResearchSpecifications(previous, options.specification)
662
+ .map((change) => ({
663
+ ...change,
664
+ ...(incomingEvidenceIds.length > 0 ? { evidenceRecordIds: incomingEvidenceIds } : {})
665
+ }));
666
+ const patchId = options.patch?.id ?? createId("spec_patch");
667
+ const revisionId = createId("spec_revision");
668
+ const patchTitle = options.title ?? options.patch?.title ?? `Research Specification update: ${options.specification.title}`;
669
+ const patchRationale = options.rationale ?? options.patch?.rationale;
670
+ const sectionEvidence = {
671
+ ...(previous?.sectionEvidence ?? {}),
672
+ ...(options.specification.sectionEvidence ?? {})
673
+ };
674
+ for (const change of changes) {
675
+ const fieldEvidenceIds = change.evidenceRecordIds ?? [];
676
+ if (fieldEvidenceIds.length > 0) {
677
+ sectionEvidence[change.path] = mergeStringLists(sectionEvidence[change.path], fieldEvidenceIds);
678
+ }
679
+ }
680
+ const specification = {
681
+ ...cloneResearchSpecification(options.specification),
682
+ updatedAt: options.timestamp,
683
+ latestRevisionId: revisionId,
684
+ sourceEvidenceIds,
685
+ sectionEvidence
686
+ };
687
+ const decision = options.decisionRecordId || options.createDecisionRecord === false
688
+ ? undefined
689
+ : {
690
+ id: createId("decision"),
691
+ timestamp: options.timestamp,
692
+ checkpointKey: "research_specification_auto_update",
693
+ level: "log_only",
694
+ mode: "commit",
695
+ summary: `Applied Research Specification update: ${patchTitle}`,
696
+ commitmentFamily: "scope",
697
+ epistemicBasis: "mixed",
698
+ rationale: patchRationale ?? "Automatically applied a source-mapped Research Specification update."
699
+ };
700
+ const decisionRecordId = options.decisionRecordId ?? decision?.id;
701
+ const revision = {
702
+ id: revisionId,
703
+ index: (state.specRevisions ?? []).length + 1,
704
+ createdAt: options.timestamp,
705
+ source: options.source,
706
+ title: patchTitle,
707
+ status: specification.status ?? "draft",
708
+ patchId,
709
+ ...(options.questionRecordId ? { questionRecordId: options.questionRecordId } : {}),
710
+ ...(decisionRecordId ? { decisionRecordId } : {}),
711
+ sourceEvidenceIds,
712
+ changeSummary: changeSummaryForRevision(changes),
713
+ specification
714
+ };
715
+ const patch = {
716
+ id: patchId,
717
+ createdAt: options.patch?.createdAt ?? options.timestamp,
718
+ updatedAt: options.timestamp,
719
+ status: "applied",
720
+ source: options.source,
721
+ title: patchTitle,
722
+ ...(patchRationale ? { rationale: patchRationale } : {}),
723
+ changes,
724
+ sourceEvidenceIds,
725
+ targetSpecification: specification,
726
+ appliedAt: options.timestamp,
727
+ appliedRevisionId: revision.id,
728
+ ...(options.questionRecordId ? { questionRecordId: options.questionRecordId } : {}),
729
+ ...(decisionRecordId ? { decisionRecordId } : {})
730
+ };
731
+ const incorporatedEvidence = (state.evidenceRecords ?? []).map((record) => sourceEvidenceIds.includes(record.id)
732
+ ? {
733
+ ...record,
734
+ incorporatedAt: options.timestamp,
735
+ incorporatedByPatchId: patch.id,
736
+ incorporatedByRevisionId: revision.id
737
+ }
738
+ : record);
739
+ const withDecision = decision ? appendDecisionToResearchState(state, decision) : state;
740
+ const previousPatches = withDecision.specPatches ?? [];
741
+ const specPatches = previousPatches.some((entry) => entry.id === patch.id)
742
+ ? previousPatches.map((entry) => entry.id === patch.id ? patch : entry)
743
+ : [...previousPatches, patch];
744
+ const nextState = {
745
+ ...withDecision,
746
+ researchSpecification: specification,
747
+ evidenceRecords: incorporatedEvidence,
748
+ specPatches,
749
+ specRevisions: [...(withDecision.specRevisions ?? []), revision],
750
+ workingState: {
751
+ ...withDecision.workingState,
752
+ researchSpecification: specification
753
+ }
754
+ };
755
+ return {
756
+ state: appendSpecGapQuestionIfNeeded(nextState, specification, options.timestamp, sourceEvidenceIds),
757
+ specification,
758
+ patch,
759
+ revision,
760
+ ...(decision ? { decision } : {})
761
+ };
762
+ }
424
763
  function summarizeWorkspaceInspection(context, state) {
425
764
  const questions = state.questionLog ?? [];
426
765
  const pendingQuestions = questions.filter((record) => record.status === "pending");
@@ -465,7 +804,11 @@ function summarizeWorkspaceInspection(context, state) {
465
804
  pendingQuestions: pendingQuestions.length,
466
805
  pendingObligations: pendingObligations.length,
467
806
  answeredQuestions: answeredQuestions.length,
468
- decisions: (state.decisionLog ?? []).length
807
+ decisions: (state.decisionLog ?? []).length,
808
+ interviewTurns: (state.interviewTurns ?? []).length,
809
+ evidenceRecords: (state.evidenceRecords ?? []).length,
810
+ specPatches: (state.specPatches ?? []).length,
811
+ specRevisions: (state.specRevisions ?? []).length
469
812
  },
470
813
  recentInvocations: recentInvocationRecords(state, 5).map((record) => ({
471
814
  id: record.id,
@@ -684,19 +1027,92 @@ export async function syncCurrentWorkspaceView(context) {
684
1027
  ? { researchSpecification: context.session.researchSpecification ?? state.researchSpecification }
685
1028
  : {})
686
1029
  };
687
- const body = buildCurrentGuide(context.project, session, recentInvocationRecords(state), recentPendingQuestions(state), recentPendingObligations(state));
1030
+ const body = buildCurrentGuide(context.project, session, recentInvocationRecords(state), recentPendingQuestions(state), recentPendingObligations(state), state);
688
1031
  await writeFile(context.currentFilePath, body, "utf8");
689
1032
  return context.currentFilePath;
690
1033
  }
1034
+ function evidenceKindForInvocationRole(role) {
1035
+ const normalized = role?.toLowerCase() ?? "";
1036
+ if (normalized.includes("critic")) {
1037
+ return "critic";
1038
+ }
1039
+ if (normalized.includes("reviewer") || normalized.includes("review")) {
1040
+ return "reviewer";
1041
+ }
1042
+ return "panel";
1043
+ }
1044
+ function evidenceRecordsForInvocation(invocation, timestamp) {
1045
+ const records = [];
1046
+ if (invocation.panelResult) {
1047
+ for (const member of invocation.panelResult.memberResults) {
1048
+ if (!member.summary && (member.claims ?? []).length === 0 && (member.objections ?? []).length === 0) {
1049
+ continue;
1050
+ }
1051
+ records.push({
1052
+ id: createId("evidence"),
1053
+ createdAt: timestamp,
1054
+ sourceKind: evidenceKindForInvocationRole(member.role),
1055
+ sourceId: `${invocation.id}:${member.role}`,
1056
+ role: member.role,
1057
+ summary: compactLine(member.summary ?? [...(member.claims ?? []), ...(member.objections ?? [])].join(" ")),
1058
+ rawText: [
1059
+ member.summary ? `Summary: ${member.summary}` : "",
1060
+ member.claims?.length ? `Claims: ${member.claims.join("; ")}` : "",
1061
+ member.objections?.length ? `Objections: ${member.objections.join("; ")}` : "",
1062
+ member.openQuestions?.length ? `Open questions: ${member.openQuestions.join("; ")}` : ""
1063
+ ].filter(Boolean).join("\n"),
1064
+ linkedInvocationRecordIds: [invocation.id],
1065
+ linkedQuestionRecordIds: invocation.panelResult.linkedQuestionRecordIds,
1066
+ linkedDecisionRecordIds: invocation.panelResult.linkedDecisionRecordIds
1067
+ });
1068
+ }
1069
+ if (invocation.panelResult.synthesis || invocation.panelResult.conflictSummary) {
1070
+ records.push({
1071
+ id: createId("evidence"),
1072
+ createdAt: timestamp,
1073
+ sourceKind: "panel",
1074
+ sourceId: invocation.panelResult.id,
1075
+ summary: compactLine(invocation.panelResult.synthesis ?? invocation.panelResult.conflictSummary ?? "Panel result"),
1076
+ rawText: [
1077
+ invocation.panelResult.synthesis ? `Synthesis: ${invocation.panelResult.synthesis}` : "",
1078
+ invocation.panelResult.conflictSummary ? `Conflict: ${invocation.panelResult.conflictSummary}` : ""
1079
+ ].filter(Boolean).join("\n"),
1080
+ linkedInvocationRecordIds: [invocation.id],
1081
+ linkedQuestionRecordIds: invocation.panelResult.linkedQuestionRecordIds,
1082
+ linkedDecisionRecordIds: invocation.panelResult.linkedDecisionRecordIds
1083
+ });
1084
+ }
1085
+ return records;
1086
+ }
1087
+ if (invocation.status === "completed" && invocation.intent.prompt.trim()) {
1088
+ records.push({
1089
+ id: createId("evidence"),
1090
+ createdAt: timestamp,
1091
+ sourceKind: "invocation",
1092
+ sourceId: invocation.id,
1093
+ summary: compactLine(`${invocation.intent.kind}/${invocation.intent.mode}: ${invocation.intent.prompt}`),
1094
+ rawText: invocation.intent.prompt,
1095
+ linkedInvocationRecordIds: [invocation.id]
1096
+ });
1097
+ }
1098
+ return records;
1099
+ }
691
1100
  export async function appendInvocationRecordToWorkspace(context, invocation, questions = []) {
692
1101
  const state = await loadResearchState(context.stateFilePath);
693
1102
  const withInvocation = appendInvocationToResearchState(state, invocation);
694
1103
  const updated = questions.length > 0
695
1104
  ? appendQuestionRecords(withInvocation, questions)
696
1105
  : withInvocation;
697
- await writeFile(context.stateFilePath, JSON.stringify(updated, null, 2), "utf8");
1106
+ const evidenceRecords = evidenceRecordsForInvocation(invocation, nowIso());
1107
+ const withEvidence = evidenceRecords.length > 0
1108
+ ? {
1109
+ ...updated,
1110
+ evidenceRecords: [...(updated.evidenceRecords ?? []), ...evidenceRecords]
1111
+ }
1112
+ : updated;
1113
+ await writeFile(context.stateFilePath, JSON.stringify(withEvidence, null, 2), "utf8");
698
1114
  await syncCurrentWorkspaceView(context);
699
- return updated;
1115
+ return withEvidence;
700
1116
  }
701
1117
  function createId(prefix) {
702
1118
  return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
@@ -842,7 +1258,24 @@ export async function appendLongTableInterviewTurn(options) {
842
1258
  : [])
843
1259
  ]
844
1260
  };
845
- const updated = upsertHook(state, hook);
1261
+ const evidence = {
1262
+ id: createId("evidence"),
1263
+ createdAt: timestamp,
1264
+ sourceKind: "interview_turn",
1265
+ sourceId: turn.id,
1266
+ sourceHookId: existing.id,
1267
+ summary: compactLine(`Interview turn ${turn.index}: ${turn.answer}`),
1268
+ rawText: [
1269
+ `Question: ${turn.question}`,
1270
+ `Answer: ${turn.answer}`,
1271
+ turn.reflection ? `Reflection: ${turn.reflection}` : ""
1272
+ ].filter(Boolean).join("\n")
1273
+ };
1274
+ const updated = {
1275
+ ...upsertHook(state, hook),
1276
+ interviewTurns: [...(state.interviewTurns ?? []), turn],
1277
+ evidenceRecords: [...(state.evidenceRecords ?? []), evidence]
1278
+ };
846
1279
  await writeFile(options.context.stateFilePath, JSON.stringify(updated, null, 2), "utf8");
847
1280
  await syncCurrentWorkspaceView(options.context);
848
1281
  return { hook, turn, state: updated };
@@ -1009,12 +1442,18 @@ export async function summarizeLongTableResearchSpecification(options) {
1009
1442
  resumeHint: `I want to continue from the Research Specification: ${specification.title}.`
1010
1443
  };
1011
1444
  options.context.session = session;
1012
- let updated = hook ? upsertHook(state, hook) : state;
1013
- updated.researchSpecification = specification;
1014
- updated.workingState = {
1015
- ...updated.workingState,
1016
- researchSpecification: specification
1017
- };
1445
+ const sourceEvidenceIds = (state.evidenceRecords ?? [])
1446
+ .filter((record) => record.sourceHookId && record.sourceHookId === (existing?.id ?? sourceHookId))
1447
+ .map((record) => record.id);
1448
+ const audited = applyResearchSpecificationAuditUpdate(hook ? upsertHook(state, hook) : state, {
1449
+ specification,
1450
+ timestamp,
1451
+ source: "interview",
1452
+ title: `Research Specification draft: ${specification.title}`,
1453
+ rationale: "Stored or refreshed the required Research Specification from LongTable interview evidence.",
1454
+ sourceEvidenceIds
1455
+ });
1456
+ let updated = audited.state;
1018
1457
  updated.narrativeTraces.push({
1019
1458
  id: createId("narrative_trace"),
1020
1459
  timestamp,
@@ -1029,6 +1468,93 @@ export async function summarizeLongTableResearchSpecification(options) {
1029
1468
  await syncCurrentWorkspaceView(options.context);
1030
1469
  return { hook, specification, state: updated, session };
1031
1470
  }
1471
+ export async function proposeResearchSpecificationPatch(options) {
1472
+ const state = await loadResearchState(options.context.stateFilePath);
1473
+ const timestamp = nowIso();
1474
+ const specification = normalizeResearchSpecification(options.specification, options.specification.sourceHookId ?? state.researchSpecification?.sourceHookId, timestamp);
1475
+ const sourceEvidenceIds = mergeStringLists(options.specification.sourceEvidenceIds, specification.sourceEvidenceIds, options.sourceEvidenceIds);
1476
+ const changes = diffResearchSpecifications(state.researchSpecification, specification)
1477
+ .map((change) => ({
1478
+ ...change,
1479
+ evidenceRecordIds: sourceEvidenceIds
1480
+ }));
1481
+ const patch = {
1482
+ id: createId("spec_patch"),
1483
+ createdAt: timestamp,
1484
+ updatedAt: timestamp,
1485
+ status: "proposed",
1486
+ source: options.source ?? "manual",
1487
+ title: `Proposed Research Specification update: ${specification.title}`,
1488
+ ...(options.rationale ? { rationale: options.rationale } : {}),
1489
+ changes,
1490
+ sourceEvidenceIds,
1491
+ targetSpecification: specification
1492
+ };
1493
+ const updated = {
1494
+ ...state,
1495
+ specPatches: [...(state.specPatches ?? []), patch]
1496
+ };
1497
+ await writeFile(options.context.stateFilePath, JSON.stringify(updated, null, 2), "utf8");
1498
+ await syncCurrentWorkspaceView(options.context);
1499
+ return { patch, changes, state: updated };
1500
+ }
1501
+ export async function applyResearchSpecificationPatch(options) {
1502
+ const state = await loadResearchState(options.context.stateFilePath);
1503
+ const timestamp = nowIso();
1504
+ const storedPatch = options.patchId
1505
+ ? (state.specPatches ?? []).find((patch) => patch.id === options.patchId)
1506
+ : undefined;
1507
+ if (options.patchId && !storedPatch) {
1508
+ throw new Error(`No Research Specification patch found for ${options.patchId}.`);
1509
+ }
1510
+ const inputSpecification = options.specification ?? storedPatch?.targetSpecification;
1511
+ if (!inputSpecification) {
1512
+ throw new Error(options.patchId ? `No target Research Specification found for patch ${options.patchId}.` : "Research Specification is required when no patchId is supplied.");
1513
+ }
1514
+ const specification = normalizeResearchSpecification(inputSpecification, inputSpecification.sourceHookId ?? state.researchSpecification?.sourceHookId, timestamp);
1515
+ const audited = applyResearchSpecificationAuditUpdate(state, {
1516
+ specification,
1517
+ timestamp,
1518
+ source: options.source ?? storedPatch?.source ?? "manual",
1519
+ title: storedPatch?.title ?? `Applied Research Specification update: ${specification.title}`,
1520
+ rationale: options.rationale ?? storedPatch?.rationale,
1521
+ sourceEvidenceIds: mergeStringLists(storedPatch?.sourceEvidenceIds, options.sourceEvidenceIds),
1522
+ patch: storedPatch,
1523
+ questionRecordId: options.questionRecordId ?? storedPatch?.questionRecordId,
1524
+ decisionRecordId: options.decisionRecordId ?? storedPatch?.decisionRecordId
1525
+ });
1526
+ const session = {
1527
+ ...options.context.session,
1528
+ researchSpecification: audited.specification,
1529
+ lastUpdatedAt: timestamp,
1530
+ resumeHint: `I want to continue from the Research Specification: ${audited.specification.title}.`
1531
+ };
1532
+ options.context.session = session;
1533
+ await writeFile(options.context.sessionFilePath, JSON.stringify(session, null, 2), "utf8");
1534
+ await writeFile(options.context.stateFilePath, JSON.stringify(audited.state, null, 2), "utf8");
1535
+ await syncCurrentWorkspaceView(options.context);
1536
+ return {
1537
+ patch: audited.patch,
1538
+ revision: audited.revision,
1539
+ specification: audited.specification,
1540
+ state: audited.state,
1541
+ session,
1542
+ ...(audited.decision ? { decision: audited.decision } : {})
1543
+ };
1544
+ }
1545
+ export async function readResearchSpecificationHistory(context) {
1546
+ const state = await loadResearchState(context.stateFilePath);
1547
+ return {
1548
+ ...(state.researchSpecification ? { specification: state.researchSpecification } : {}),
1549
+ revisions: state.specRevisions ?? [],
1550
+ patches: state.specPatches ?? [],
1551
+ evidenceRecords: state.evidenceRecords ?? []
1552
+ };
1553
+ }
1554
+ export async function findUnincorporatedResearchEvidence(context) {
1555
+ const state = await loadResearchState(context.stateFilePath);
1556
+ return (state.evidenceRecords ?? []).filter((record) => !record.incorporatedByRevisionId);
1557
+ }
1032
1558
  function findQuestionForDecision(state, questionId) {
1033
1559
  const pending = (state.questionLog ?? []).filter((record) => record.status === "pending");
1034
1560
  if (questionId) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@longtable/cli",
3
- "version": "0.1.47",
3
+ "version": "0.1.48",
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.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"
32
+ "@longtable/checkpoints": "0.1.48",
33
+ "@longtable/core": "0.1.48",
34
+ "@longtable/memory": "0.1.48",
35
+ "@longtable/provider-claude": "0.1.48",
36
+ "@longtable/provider-codex": "0.1.48",
37
+ "@longtable/setup": "0.1.48"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@types/node": "^22.10.1",