@longtable/cli 0.1.45 → 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 +238 -9
- package/dist/project-session.d.ts +72 -1
- package/dist/project-session.js +717 -46
- package/package.json +7 -7
package/dist/project-session.js
CHANGED
|
@@ -163,7 +163,103 @@ function renderResearchSpecificationSummary(specification, locale) {
|
|
|
163
163
|
}
|
|
164
164
|
return lines;
|
|
165
165
|
}
|
|
166
|
-
function
|
|
166
|
+
function renderResearchSpecificationStatus(session, locale) {
|
|
167
|
+
if (!session.firstResearchShape && !session.researchSpecification) {
|
|
168
|
+
return [];
|
|
169
|
+
}
|
|
170
|
+
const korean = locale === "ko";
|
|
171
|
+
if (!session.researchSpecification) {
|
|
172
|
+
return [
|
|
173
|
+
"",
|
|
174
|
+
korean ? "## Research Specification 상태" : "## Research Specification Status",
|
|
175
|
+
korean
|
|
176
|
+
? "- 상태: First Research Shape는 있지만 Research Specification은 아직 없습니다."
|
|
177
|
+
: "- Status: First Research Shape exists, but Research Specification is missing.",
|
|
178
|
+
korean
|
|
179
|
+
? "- 의미: First Research Shape는 짧은 핸들/재개 인덱스이며, 인터뷰 종료나 연구 명세 확정이 아닙니다."
|
|
180
|
+
: "- Meaning: First Research Shape is a short handle/resume index, not interview closure or a confirmed research specification.",
|
|
181
|
+
korean
|
|
182
|
+
? "- 다음 프로토콜: 충분한 내용이 있으면 `summarize_research_specification`으로 preview를 만들고 `confirm_research_specification`으로 저장/한 질문 더/섹션 수정/열어두기를 확인합니다."
|
|
183
|
+
: "- Next protocol: when enough detail exists, run `summarize_research_specification` to create the preview, then `confirm_research_specification` to confirm, ask one more question, revise a section, or keep it open."
|
|
184
|
+
];
|
|
185
|
+
}
|
|
186
|
+
const status = session.researchSpecification.confirmedAt
|
|
187
|
+
? "confirmed"
|
|
188
|
+
: session.researchSpecification.status ?? "draft";
|
|
189
|
+
if (status === "confirmed") {
|
|
190
|
+
return [];
|
|
191
|
+
}
|
|
192
|
+
return [
|
|
193
|
+
"",
|
|
194
|
+
korean ? "## Research Specification 상태" : "## Research Specification Status",
|
|
195
|
+
korean
|
|
196
|
+
? `- 상태: ${status}. Research Specification은 저장되어 있지만 아직 확정된 종료 지점이 아닙니다.`
|
|
197
|
+
: `- Status: ${status}. Research Specification exists, but it is not a confirmed closure point yet.`,
|
|
198
|
+
korean
|
|
199
|
+
? "- 다음 프로토콜: 명세를 업데이트한 뒤 `confirm_research_specification`으로 다시 preview 확인을 받아야 합니다."
|
|
200
|
+
: "- Next protocol: update the specification, then return to `confirm_research_specification` for another preview confirmation."
|
|
201
|
+
];
|
|
202
|
+
}
|
|
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()) {
|
|
167
263
|
const locale = normalizeLocale(session.locale ?? project.locale);
|
|
168
264
|
const openQuestions = session.openQuestions && session.openQuestions.length > 0
|
|
169
265
|
? session.openQuestions
|
|
@@ -191,6 +287,8 @@ function buildCurrentGuide(project, session, recentInvocations = [], pendingQues
|
|
|
191
287
|
`- 다음 액션: ${nextAction}`,
|
|
192
288
|
`- 관점: ${session.requestedPerspectives.length > 0 ? session.requestedPerspectives.join(", ") : "auto"}`,
|
|
193
289
|
`- disagreement: ${session.disagreementPreference}`,
|
|
290
|
+
...renderResearchSpecificationStatus(session, locale),
|
|
291
|
+
...renderResearchSpecificationAudit(state, locale),
|
|
194
292
|
"",
|
|
195
293
|
"## 열린 질문",
|
|
196
294
|
...openQuestions.map((question) => `- ${question}`),
|
|
@@ -210,7 +308,7 @@ function buildCurrentGuide(project, session, recentInvocations = [], pendingQues
|
|
|
210
308
|
"## 대기 중인 결정 질문",
|
|
211
309
|
...pendingQuestions.map((record) => {
|
|
212
310
|
const options = formatQuestionOptionValues(record).join("/");
|
|
213
|
-
return `- ${record.id}: ${record.prompt.question} (${options})`;
|
|
311
|
+
return `- ${record.id}: ${record.prompt.question}${formatQuestionMetadata(record)} (${options})`;
|
|
214
312
|
}),
|
|
215
313
|
"- 답변 기록: `longtable decide --question <id> --answer <value>`"
|
|
216
314
|
]
|
|
@@ -266,6 +364,8 @@ function buildCurrentGuide(project, session, recentInvocations = [], pendingQues
|
|
|
266
364
|
`- Next action: ${nextAction}`,
|
|
267
365
|
`- Perspectives: ${session.requestedPerspectives.length > 0 ? session.requestedPerspectives.join(", ") : "auto"}`,
|
|
268
366
|
`- Disagreement: ${session.disagreementPreference}`,
|
|
367
|
+
...renderResearchSpecificationStatus(session, locale),
|
|
368
|
+
...renderResearchSpecificationAudit(state, locale),
|
|
269
369
|
"",
|
|
270
370
|
"## Open Questions",
|
|
271
371
|
...openQuestions.map((question) => `- ${question}`),
|
|
@@ -285,7 +385,7 @@ function buildCurrentGuide(project, session, recentInvocations = [], pendingQues
|
|
|
285
385
|
"## Pending Decision Questions",
|
|
286
386
|
...pendingQuestions.map((record) => {
|
|
287
387
|
const options = formatQuestionOptionValues(record).join("/");
|
|
288
|
-
return `- ${record.id}: ${record.prompt.question} (${options})`;
|
|
388
|
+
return `- ${record.id}: ${record.prompt.question}${formatQuestionMetadata(record)} (${options})`;
|
|
289
389
|
}),
|
|
290
390
|
"- Record an answer: `longtable decide --question <id> --answer <value>`"
|
|
291
391
|
]
|
|
@@ -334,6 +434,10 @@ async function loadResearchState(stateFilePath) {
|
|
|
334
434
|
hooks: parsed.hooks ?? [],
|
|
335
435
|
...(parsed.firstResearchShape ? { firstResearchShape: parsed.firstResearchShape } : {}),
|
|
336
436
|
...(parsed.researchSpecification ? { researchSpecification: parsed.researchSpecification } : {}),
|
|
437
|
+
interviewTurns: parsed.interviewTurns ?? [],
|
|
438
|
+
evidenceRecords: parsed.evidenceRecords ?? [],
|
|
439
|
+
specPatches: parsed.specPatches ?? [],
|
|
440
|
+
specRevisions: parsed.specRevisions ?? [],
|
|
337
441
|
questionObligations: parsed.questionObligations ?? [],
|
|
338
442
|
inferredHypotheses: parsed.inferredHypotheses ?? [],
|
|
339
443
|
openTensions: parsed.openTensions ?? [],
|
|
@@ -375,6 +479,287 @@ function formatQuestionOptionValues(record) {
|
|
|
375
479
|
}
|
|
376
480
|
return values;
|
|
377
481
|
}
|
|
482
|
+
function formatQuestionMetadata(record) {
|
|
483
|
+
const parts = [
|
|
484
|
+
record.commitmentFamily ? `commitment: ${record.commitmentFamily}` : "",
|
|
485
|
+
record.epistemicBasis ? `basis: ${record.epistemicBasis}` : ""
|
|
486
|
+
].filter(Boolean);
|
|
487
|
+
return parts.length > 0 ? ` [${parts.join("; ")}]` : "";
|
|
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
|
+
}
|
|
378
763
|
function summarizeWorkspaceInspection(context, state) {
|
|
379
764
|
const questions = state.questionLog ?? [];
|
|
380
765
|
const pendingQuestions = questions.filter((record) => record.status === "pending");
|
|
@@ -419,7 +804,11 @@ function summarizeWorkspaceInspection(context, state) {
|
|
|
419
804
|
pendingQuestions: pendingQuestions.length,
|
|
420
805
|
pendingObligations: pendingObligations.length,
|
|
421
806
|
answeredQuestions: answeredQuestions.length,
|
|
422
|
-
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
|
|
423
812
|
},
|
|
424
813
|
recentInvocations: recentInvocationRecords(state, 5).map((record) => ({
|
|
425
814
|
id: record.id,
|
|
@@ -435,6 +824,8 @@ function summarizeWorkspaceInspection(context, state) {
|
|
|
435
824
|
id: record.id,
|
|
436
825
|
title: record.prompt.title,
|
|
437
826
|
question: record.prompt.question,
|
|
827
|
+
...(record.commitmentFamily ? { commitmentFamily: record.commitmentFamily } : {}),
|
|
828
|
+
...(record.epistemicBasis ? { epistemicBasis: record.epistemicBasis } : {}),
|
|
438
829
|
options: formatQuestionOptionValues(record),
|
|
439
830
|
required: record.prompt.required
|
|
440
831
|
})),
|
|
@@ -449,6 +840,8 @@ function summarizeWorkspaceInspection(context, state) {
|
|
|
449
840
|
id: record.id,
|
|
450
841
|
checkpointKey: record.checkpointKey,
|
|
451
842
|
summary: record.summary,
|
|
843
|
+
...(record.commitmentFamily ? { commitmentFamily: record.commitmentFamily } : {}),
|
|
844
|
+
...(record.epistemicBasis ? { epistemicBasis: record.epistemicBasis } : {}),
|
|
452
845
|
...(record.selectedOption ? { selectedOption: record.selectedOption } : {}),
|
|
453
846
|
timestamp: record.timestamp
|
|
454
847
|
})),
|
|
@@ -495,9 +888,12 @@ function buildProjectAgentsMd(project, session) {
|
|
|
495
888
|
"- Begin exploratory work with clarifying or tension questions before recommending a direction.",
|
|
496
889
|
"- For `$longtable-interview`, ask one natural-language question at a time, reflect with `LongTable hears: ...`, record turns when MCP is available, and avoid early reader/reviewer or theory/method/measurement classification.",
|
|
497
890
|
"- Do not summarize `$longtable-interview` because a fixed number of turns has passed; wait for content-based readiness around research object, focal uncertainty, boundary, evidence/material, protected decision, and next action.",
|
|
891
|
+
"- First Research Shape is a short handle/resume index, not the default closure point.",
|
|
498
892
|
"- After the First Research Shape, create a Research Specification when the interview has enough detail to preserve scope, construct ontology, theory framing, coding rules, method options, evidence/access requirements, epistemic alignment, protected decisions, open questions, and next actions.",
|
|
893
|
+
"- If a confirmed First Research Shape exists without a Research Specification, continue directly into the next Research Specification question instead of asking shape-level continue/revise/restart questions.",
|
|
894
|
+
"- If the researcher chooses `ask_one_more` or `revise_section` at Research Specification confirmation, answer that gap and return to the Research Specification Preview before ending the interview.",
|
|
499
895
|
"- Do not let unrelated pending Researcher Checkpoints interrupt `$longtable-interview`; mention them only as separate unresolved checkpoints unless the researcher is confirming, saving, or recording a research decision.",
|
|
500
|
-
"- Use structured options
|
|
896
|
+
"- Use structured options at the final Research Specification confirmation, at explicit short-handle stop points, or at true checkpoint boundaries.",
|
|
501
897
|
"- If you foreground role perspectives, disclose them with `LongTable consulted: ...`.",
|
|
502
898
|
"- Keep one accountable synthesis, but do not hide meaningful disagreement.",
|
|
503
899
|
...(session.disagreementPreference === "always_visible"
|
|
@@ -631,19 +1027,92 @@ export async function syncCurrentWorkspaceView(context) {
|
|
|
631
1027
|
? { researchSpecification: context.session.researchSpecification ?? state.researchSpecification }
|
|
632
1028
|
: {})
|
|
633
1029
|
};
|
|
634
|
-
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);
|
|
635
1031
|
await writeFile(context.currentFilePath, body, "utf8");
|
|
636
1032
|
return context.currentFilePath;
|
|
637
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
|
+
}
|
|
638
1100
|
export async function appendInvocationRecordToWorkspace(context, invocation, questions = []) {
|
|
639
1101
|
const state = await loadResearchState(context.stateFilePath);
|
|
640
1102
|
const withInvocation = appendInvocationToResearchState(state, invocation);
|
|
641
1103
|
const updated = questions.length > 0
|
|
642
1104
|
? appendQuestionRecords(withInvocation, questions)
|
|
643
1105
|
: withInvocation;
|
|
644
|
-
|
|
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");
|
|
645
1114
|
await syncCurrentWorkspaceView(context);
|
|
646
|
-
return
|
|
1115
|
+
return withEvidence;
|
|
647
1116
|
}
|
|
648
1117
|
function createId(prefix) {
|
|
649
1118
|
return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
@@ -789,7 +1258,24 @@ export async function appendLongTableInterviewTurn(options) {
|
|
|
789
1258
|
: [])
|
|
790
1259
|
]
|
|
791
1260
|
};
|
|
792
|
-
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
|
+
};
|
|
793
1279
|
await writeFile(options.context.stateFilePath, JSON.stringify(updated, null, 2), "utf8");
|
|
794
1280
|
await syncCurrentWorkspaceView(options.context);
|
|
795
1281
|
return { hook, turn, state: updated };
|
|
@@ -956,12 +1442,18 @@ export async function summarizeLongTableResearchSpecification(options) {
|
|
|
956
1442
|
resumeHint: `I want to continue from the Research Specification: ${specification.title}.`
|
|
957
1443
|
};
|
|
958
1444
|
options.context.session = session;
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
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;
|
|
965
1457
|
updated.narrativeTraces.push({
|
|
966
1458
|
id: createId("narrative_trace"),
|
|
967
1459
|
timestamp,
|
|
@@ -976,6 +1468,93 @@ export async function summarizeLongTableResearchSpecification(options) {
|
|
|
976
1468
|
await syncCurrentWorkspaceView(options.context);
|
|
977
1469
|
return { hook, specification, state: updated, session };
|
|
978
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
|
+
}
|
|
979
1558
|
function findQuestionForDecision(state, questionId) {
|
|
980
1559
|
const pending = (state.questionLog ?? []).filter((record) => record.status === "pending");
|
|
981
1560
|
if (questionId) {
|
|
@@ -1668,6 +2247,70 @@ export function generateQuestionOpportunities(prompt, options = {}) {
|
|
|
1668
2247
|
};
|
|
1669
2248
|
}
|
|
1670
2249
|
const FOLLOW_UP_PROMPT_PREFIX = "Follow-up prompt:";
|
|
2250
|
+
function compactMetadataText(parts) {
|
|
2251
|
+
return parts
|
|
2252
|
+
.flatMap((part) => Array.isArray(part) ? part : [part])
|
|
2253
|
+
.filter((part) => Boolean(part && part.trim()))
|
|
2254
|
+
.join(" ")
|
|
2255
|
+
.replace(/\s+/g, " ")
|
|
2256
|
+
.trim()
|
|
2257
|
+
.toLowerCase();
|
|
2258
|
+
}
|
|
2259
|
+
function textMatchesAny(text, patterns) {
|
|
2260
|
+
return patterns.some((pattern) => pattern.test(text));
|
|
2261
|
+
}
|
|
2262
|
+
const COMMITMENT_FAMILY_BY_CHECKPOINT = [
|
|
2263
|
+
[/product|meta_decision/, "product_policy"],
|
|
2264
|
+
[/research_question|research_direction|scope|boundary|inclusion|exclusion/, "scope"],
|
|
2265
|
+
[/theory|construct|conceptual/, "construct"],
|
|
2266
|
+
[/measurement|coding|codebook|extraction/, "coding"],
|
|
2267
|
+
[/method|analysis|panel_disagreement|team_debate|review/, "method"],
|
|
2268
|
+
[/evidence|scholarly_access|source_authority/, "evidence"],
|
|
2269
|
+
[/knowledge_gap|tacit_assumption|epistemic/, "epistemic_authority"]
|
|
2270
|
+
];
|
|
2271
|
+
function inferCommitmentFamily(input) {
|
|
2272
|
+
const checkpointKey = (input.checkpointKey ?? "").toLowerCase();
|
|
2273
|
+
const matched = COMMITMENT_FAMILY_BY_CHECKPOINT.find(([pattern]) => pattern.test(checkpointKey));
|
|
2274
|
+
if (matched)
|
|
2275
|
+
return matched[1];
|
|
2276
|
+
if (input.triggerFamily === "meta_decision")
|
|
2277
|
+
return "product_policy";
|
|
2278
|
+
if (input.triggerFamily === "evidence")
|
|
2279
|
+
return "evidence";
|
|
2280
|
+
const text = compactMetadataText([input.title, input.question, input.prompt, input.rationale]);
|
|
2281
|
+
if (textMatchesAny(text, [/checkpoint policy/, /hook ux/, /product language/, /\breadme\b/, /제품 언어|체크포인트 정책|훅|리드미/])) {
|
|
2282
|
+
return "product_policy";
|
|
2283
|
+
}
|
|
2284
|
+
return undefined;
|
|
2285
|
+
}
|
|
2286
|
+
function inferEpistemicBasis(input) {
|
|
2287
|
+
const text = compactMetadataText([input.title, input.question, input.prompt, input.rationale]);
|
|
2288
|
+
const bases = [];
|
|
2289
|
+
if (textMatchesAny(text, [/\bresearcher\b/, /\bhuman\b/, /\byour judgment\b/, /\byour knowledge\b/, /연구자|인간|사람|너의\s*판단|당신의\s*판단|내\s*지식|사용자/])) {
|
|
2290
|
+
bases.push("researcher_knowledge");
|
|
2291
|
+
}
|
|
2292
|
+
if (textMatchesAny(text, [/\bproject state\b/, /\bworkspace\b/, /\bcurrent\.md\b/, /\.longtable\b/, /\bstate\.json\b/, /\bdataset\b/, /\bcodebook\b/, /\bcoding sheet\b/, /프로젝트\s*상태|워크스페이스|데이터셋|코드북|코딩\s*시트/])) {
|
|
2293
|
+
bases.push("project_state");
|
|
2294
|
+
}
|
|
2295
|
+
if (textMatchesAny(text, [/\bexternal evidence\b/, /\bliterature\b/, /\bpaper\b/, /\bpdf\b/, /\bsource\b/, /\bcitation\b/, /\breference\b/, /\bfull[- ]?text\b/, /외부\s*근거|문헌|논문|원문|전문|출처|인용|레퍼런스/])) {
|
|
2296
|
+
bases.push("external_evidence");
|
|
2297
|
+
}
|
|
2298
|
+
if (textMatchesAny(text, [/\bcodex\b/, /\bllm\b/, /\blanguage model\b/, /\bmodel judgment\b/, /\bai inference\b/, /\bassistant judgment\b/, /코덱스|언어\s*모델|모델\s*판단|AI\s*추론|LLM/])) {
|
|
2299
|
+
bases.push("ai_inference");
|
|
2300
|
+
}
|
|
2301
|
+
const unique = [...new Set(bases)];
|
|
2302
|
+
if (unique.length > 1)
|
|
2303
|
+
return "mixed";
|
|
2304
|
+
return unique[0];
|
|
2305
|
+
}
|
|
2306
|
+
function resolveQuestionRecordMetadata(input) {
|
|
2307
|
+
const commitmentFamily = input.commitmentFamily ?? inferCommitmentFamily(input);
|
|
2308
|
+
const epistemicBasis = input.epistemicBasis ?? inferEpistemicBasis(input);
|
|
2309
|
+
return {
|
|
2310
|
+
...(commitmentFamily ? { commitmentFamily } : {}),
|
|
2311
|
+
...(epistemicBasis ? { epistemicBasis } : {})
|
|
2312
|
+
};
|
|
2313
|
+
}
|
|
1671
2314
|
function hasFollowUpPrompt(record, prompt) {
|
|
1672
2315
|
return record.prompt.rationale.includes(`${FOLLOW_UP_PROMPT_PREFIX} ${prompt}`);
|
|
1673
2316
|
}
|
|
@@ -1705,31 +2348,43 @@ export async function createWorkspaceFollowUpQuestions(options) {
|
|
|
1705
2348
|
if (specsToCreate.length === 0) {
|
|
1706
2349
|
return { questions: pendingMatches, state, created: false, alreadyAnswered: false };
|
|
1707
2350
|
}
|
|
1708
|
-
const questions = specsToCreate.map((spec) =>
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
2351
|
+
const questions = specsToCreate.map((spec) => {
|
|
2352
|
+
const checkpointKey = `follow_up_${spec.key}`;
|
|
2353
|
+
const rationale = [
|
|
2354
|
+
spec.whyNow,
|
|
2355
|
+
`Question kind: ${spec.kind}`,
|
|
2356
|
+
`Question confidence: ${spec.confidence}`,
|
|
2357
|
+
`${FOLLOW_UP_PROMPT_PREFIX} ${options.prompt}`
|
|
2358
|
+
];
|
|
2359
|
+
const metadata = resolveQuestionRecordMetadata({
|
|
2360
|
+
checkpointKey,
|
|
1716
2361
|
title: spec.title,
|
|
1717
2362
|
question: spec.question,
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
2363
|
+
prompt: options.prompt,
|
|
2364
|
+
rationale
|
|
2365
|
+
});
|
|
2366
|
+
return {
|
|
2367
|
+
id: createId("question_record"),
|
|
2368
|
+
createdAt,
|
|
2369
|
+
updatedAt: createdAt,
|
|
2370
|
+
status: "pending",
|
|
2371
|
+
...metadata,
|
|
2372
|
+
prompt: {
|
|
2373
|
+
id: createId("question_prompt"),
|
|
2374
|
+
checkpointKey,
|
|
2375
|
+
title: spec.title,
|
|
2376
|
+
question: spec.question,
|
|
2377
|
+
type: "single_choice",
|
|
2378
|
+
options: spec.options,
|
|
2379
|
+
allowOther: true,
|
|
2380
|
+
otherLabel: "Other",
|
|
2381
|
+
required: options.required ?? spec.required,
|
|
2382
|
+
source: "runtime_guidance",
|
|
2383
|
+
rationale,
|
|
2384
|
+
preferredSurfaces: preferredSurfaces
|
|
2385
|
+
}
|
|
2386
|
+
};
|
|
2387
|
+
});
|
|
1733
2388
|
const updated = appendQuestionRecords(state, questions);
|
|
1734
2389
|
await writeFile(options.context.stateFilePath, JSON.stringify(updated, null, 2), "utf8");
|
|
1735
2390
|
await syncCurrentWorkspaceView(options.context);
|
|
@@ -1743,16 +2398,35 @@ export async function createWorkspaceQuestion(options) {
|
|
|
1743
2398
|
});
|
|
1744
2399
|
const checkpointKey = options.checkpointKey ?? trigger.signal.checkpointKey;
|
|
1745
2400
|
const createdAt = nowIso();
|
|
2401
|
+
const title = options.title ?? questionTitleForCheckpoint(trigger.family, checkpointKey);
|
|
2402
|
+
const questionText = options.question ?? questionTextForCheckpoint(trigger.family, options.prompt, checkpointKey);
|
|
2403
|
+
const rationale = [
|
|
2404
|
+
...trigger.rationale,
|
|
2405
|
+
`Trigger family: ${trigger.family}.`,
|
|
2406
|
+
`Trigger confidence: ${trigger.confidence}.`,
|
|
2407
|
+
`Original prompt: ${options.prompt}`
|
|
2408
|
+
];
|
|
2409
|
+
const metadata = resolveQuestionRecordMetadata({
|
|
2410
|
+
checkpointKey,
|
|
2411
|
+
triggerFamily: trigger.family,
|
|
2412
|
+
title,
|
|
2413
|
+
question: questionText,
|
|
2414
|
+
prompt: options.prompt,
|
|
2415
|
+
rationale,
|
|
2416
|
+
commitmentFamily: options.commitmentFamily,
|
|
2417
|
+
epistemicBasis: options.epistemicBasis
|
|
2418
|
+
});
|
|
1746
2419
|
const question = {
|
|
1747
2420
|
id: createId("question_record"),
|
|
1748
2421
|
createdAt,
|
|
1749
2422
|
updatedAt: createdAt,
|
|
1750
2423
|
status: "pending",
|
|
2424
|
+
...metadata,
|
|
1751
2425
|
prompt: {
|
|
1752
2426
|
id: createId("question_prompt"),
|
|
1753
2427
|
checkpointKey,
|
|
1754
|
-
title
|
|
1755
|
-
question:
|
|
2428
|
+
title,
|
|
2429
|
+
question: questionText,
|
|
1756
2430
|
type: "single_choice",
|
|
1757
2431
|
options: options.questionOptions ?? optionsForCheckpointTrigger(trigger.family, checkpointKey),
|
|
1758
2432
|
allowOther: true,
|
|
@@ -1760,12 +2434,7 @@ export async function createWorkspaceQuestion(options) {
|
|
|
1760
2434
|
required: options.required ?? trigger.requiresQuestionBeforeClosure,
|
|
1761
2435
|
source: "checkpoint",
|
|
1762
2436
|
displayReason: options.displayReason ?? trigger.rationale[0],
|
|
1763
|
-
rationale
|
|
1764
|
-
...trigger.rationale,
|
|
1765
|
-
`Trigger family: ${trigger.family}.`,
|
|
1766
|
-
`Trigger confidence: ${trigger.confidence}.`,
|
|
1767
|
-
`Original prompt: ${options.prompt}`
|
|
1768
|
-
],
|
|
2437
|
+
rationale,
|
|
1769
2438
|
preferredSurfaces: options.provider === "claude"
|
|
1770
2439
|
? ["native_structured", "numbered"]
|
|
1771
2440
|
: ["mcp_elicitation", "numbered"]
|
|
@@ -1892,6 +2561,8 @@ export async function answerWorkspaceQuestion(options) {
|
|
|
1892
2561
|
level: question.prompt.required ? "adaptive_required" : "recommended",
|
|
1893
2562
|
mode: "commit",
|
|
1894
2563
|
summary: `Answered ${question.prompt.title}: ${answer.selectedLabels.join(", ")}`,
|
|
2564
|
+
...(question.commitmentFamily ? { commitmentFamily: question.commitmentFamily } : {}),
|
|
2565
|
+
...(question.epistemicBasis ? { epistemicBasis: question.epistemicBasis } : {}),
|
|
1895
2566
|
selectedOption: answer.selectedValues[0],
|
|
1896
2567
|
...(rationale ? { rationale } : {})
|
|
1897
2568
|
};
|