@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 +120 -3
- package/dist/project-session.d.ts +66 -1
- package/dist/project-session.js +538 -12
- package/package.json +7 -7
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>;
|
package/dist/project-session.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
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.
|
|
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.
|
|
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.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",
|