@longtable/cli 0.1.47 → 0.1.49
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 +155 -15
- package/dist/codex-hooks.d.ts +9 -2
- package/dist/codex-hooks.js +175 -7
- package/dist/longtable-codex-native-hook.js +36 -0
- package/dist/project-session.d.ts +66 -1
- package/dist/project-session.js +538 -12
- package/package.json +7 -7
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.49",
|
|
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.49",
|
|
33
|
+
"@longtable/core": "0.1.49",
|
|
34
|
+
"@longtable/memory": "0.1.49",
|
|
35
|
+
"@longtable/provider-claude": "0.1.49",
|
|
36
|
+
"@longtable/provider-codex": "0.1.49",
|
|
37
|
+
"@longtable/setup": "0.1.49"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@types/node": "^22.10.1",
|