@longtable/core 0.1.51 → 0.1.53

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.
@@ -0,0 +1,24 @@
1
+ import type { HardStopScope, ResearchState } from "./types.js";
2
+ export type HardStopBlockerType = "question" | "obligation" | "spec_patch";
3
+ export interface HardStopBlocker {
4
+ type: HardStopBlockerType;
5
+ id: string;
6
+ scope: HardStopScope;
7
+ prompt: string;
8
+ reason: string;
9
+ source: string;
10
+ sourceField: string;
11
+ commandHint: string;
12
+ commandHints: string[];
13
+ nextAction: string;
14
+ priority: number;
15
+ }
16
+ export interface HardStopVerdict {
17
+ stopWouldBlock: boolean;
18
+ activeBlockers: HardStopBlocker[];
19
+ staleOrUnrelatedPendingQuestionCount: number;
20
+ stalePendingQuestionCount: number;
21
+ stalePendingObligationCount: number;
22
+ nextActions: string[];
23
+ }
24
+ export declare function collectHardStopBlockers(state: ResearchState): HardStopVerdict;
@@ -0,0 +1,243 @@
1
+ const SCOPE_PRIORITY = {
2
+ research_question: 10,
3
+ scope: 20,
4
+ construct: 30,
5
+ method: 40,
6
+ evidence: 50,
7
+ protected_decision: 60
8
+ };
9
+ const CHECKPOINT_SCOPE_PATTERNS = [
10
+ [/research_(?:question|direction|specification)|research question|question_freeze|direction_change/i, "research_question"],
11
+ [/scope|boundary|criteria|required_sections|inclusion|exclusion/i, "scope"],
12
+ [/construct|theory|framing|ontology|measurement|validity|coding/i, "construct"],
13
+ [/method|analysis|sampling|design|plan|strategy/i, "method"],
14
+ [/evidence|access|source|corpus|full[-_ ]?text|pdf|scholarly/i, "evidence"],
15
+ [/protected_decision|protected decision|authorship_voice|epistemic/i, "protected_decision"]
16
+ ];
17
+ const PATCH_SCOPE_PATTERNS = [
18
+ [/researchDirection\.question/i, "research_question"],
19
+ [/researchDirection|scope|inclusion|exclusion/i, "scope"],
20
+ [/constructOntology|theoryAndFraming|measurementCoding|coding/i, "construct"],
21
+ [/methodAnalysis|analysis|design/i, "method"],
22
+ [/evidenceAccess|source|corpus|access/i, "evidence"],
23
+ [/protectedDecisions|epistemicAlignment/i, "protected_decision"]
24
+ ];
25
+ const PRODUCT_OR_TOOLING_PATTERN = /\b(product|runtime|guidance|setup|install|hook|mcp|codex|claude|skill|prompt|docs?|documentation|release|version|npm|git|github|simulation|policy|workflow|package|router|autocomplete)\b/i;
26
+ function compactText(...values) {
27
+ return values.filter(Boolean).join("\n");
28
+ }
29
+ function readBooleanFlag(value) {
30
+ return typeof value === "boolean" ? value : null;
31
+ }
32
+ function readScope(value) {
33
+ return value === "research_question" ||
34
+ value === "scope" ||
35
+ value === "construct" ||
36
+ value === "method" ||
37
+ value === "evidence" ||
38
+ value === "protected_decision"
39
+ ? value
40
+ : null;
41
+ }
42
+ function scopeFromCommitmentFamily(family) {
43
+ switch (family) {
44
+ case "scope":
45
+ return "scope";
46
+ case "construct":
47
+ case "coding":
48
+ return "construct";
49
+ case "method":
50
+ return "method";
51
+ case "evidence":
52
+ return "evidence";
53
+ case "epistemic_authority":
54
+ return "protected_decision";
55
+ default:
56
+ return null;
57
+ }
58
+ }
59
+ function scopeFromText(text) {
60
+ if (PRODUCT_OR_TOOLING_PATTERN.test(text)) {
61
+ return null;
62
+ }
63
+ return CHECKPOINT_SCOPE_PATTERNS.find(([pattern]) => pattern.test(text))?.[1] ?? null;
64
+ }
65
+ function questionSearchText(question) {
66
+ return compactText(question.prompt.checkpointKey, question.prompt.title, question.prompt.question, question.prompt.displayReason, question.commitmentFamily, question.epistemicBasis, ...(question.prompt.rationale ?? []));
67
+ }
68
+ function obligationSearchText(obligation) {
69
+ return compactText(obligation.kind, obligation.prompt, obligation.reason);
70
+ }
71
+ function inferQuestionHardStopScope(question) {
72
+ if (question.status !== "pending") {
73
+ return null;
74
+ }
75
+ const explicitHardStop = readBooleanFlag(question.hardStop);
76
+ if (explicitHardStop === false) {
77
+ return null;
78
+ }
79
+ const explicitScope = readScope(question.hardStopScope);
80
+ if (explicitHardStop === true) {
81
+ return {
82
+ scope: explicitScope ?? scopeFromCommitmentFamily(question.commitmentFamily) ?? scopeFromText(questionSearchText(question)) ?? "scope",
83
+ sourceField: explicitScope ? "QuestionRecord.hardStopScope" : "QuestionRecord.hardStop"
84
+ };
85
+ }
86
+ if (!question.prompt.required) {
87
+ return null;
88
+ }
89
+ const familyScope = scopeFromCommitmentFamily(question.commitmentFamily);
90
+ if (familyScope) {
91
+ return { scope: familyScope, sourceField: "QuestionRecord.commitmentFamily" };
92
+ }
93
+ const textScope = scopeFromText(questionSearchText(question));
94
+ return textScope ? { scope: textScope, sourceField: "QuestionRecord.derived" } : null;
95
+ }
96
+ function inferObligationHardStopScope(obligation, linkedQuestion) {
97
+ if (obligation.status !== "pending") {
98
+ return null;
99
+ }
100
+ const explicitHardStop = readBooleanFlag(obligation.hardStop);
101
+ if (explicitHardStop === false) {
102
+ return null;
103
+ }
104
+ const explicitScope = readScope(obligation.hardStopScope);
105
+ if (explicitHardStop === true) {
106
+ return {
107
+ scope: explicitScope ?? (linkedQuestion ? inferQuestionHardStopScope(linkedQuestion)?.scope : null) ?? scopeFromText(obligationSearchText(obligation)) ?? "scope",
108
+ sourceField: explicitScope ? "LongTableQuestionObligation.hardStopScope" : "LongTableQuestionObligation.hardStop"
109
+ };
110
+ }
111
+ if (obligation.kind === "research_specification_confirmation") {
112
+ return {
113
+ scope: scopeFromText(obligationSearchText(obligation)) ?? "scope",
114
+ sourceField: "LongTableQuestionObligation.derived"
115
+ };
116
+ }
117
+ if (obligation.kind === "required_question" && linkedQuestion) {
118
+ const linkedScope = inferQuestionHardStopScope(linkedQuestion);
119
+ return linkedScope
120
+ ? { scope: linkedScope.scope, sourceField: "LongTableQuestionObligation.derived" }
121
+ : null;
122
+ }
123
+ return null;
124
+ }
125
+ function scopeFromPatchChange(change) {
126
+ return PATCH_SCOPE_PATTERNS.find(([pattern]) => pattern.test(change.path))?.[1] ?? null;
127
+ }
128
+ function commandHintsForQuestion(id) {
129
+ return [
130
+ `longtable decide --question ${id} --answer <value> --rationale <why>`,
131
+ `longtable clear-question --question ${id} --reason <why safe to defer or clear>`
132
+ ];
133
+ }
134
+ function commandHintsForBlocker(type, id, questionId) {
135
+ if (questionId) {
136
+ return commandHintsForQuestion(questionId);
137
+ }
138
+ if (type === "question") {
139
+ return commandHintsForQuestion(id);
140
+ }
141
+ if (type === "obligation") {
142
+ return [`longtable obligation resolve --id ${id} --reason <why>`];
143
+ }
144
+ return [`record a DecisionRecord before applying patch ${id}`];
145
+ }
146
+ function createBlocker(input) {
147
+ const commandHints = commandHintsForBlocker(input.type, input.id, input.questionId);
148
+ const commandHint = commandHints[0] ?? "decide, clear, or defer with rationale";
149
+ return {
150
+ type: input.type,
151
+ id: input.id,
152
+ scope: input.scope,
153
+ prompt: input.prompt,
154
+ reason: input.reason,
155
+ source: input.sourceField,
156
+ sourceField: input.sourceField,
157
+ commandHint,
158
+ commandHints,
159
+ nextAction: commandHint,
160
+ priority: SCOPE_PRIORITY[input.scope] + input.priorityOffset
161
+ };
162
+ }
163
+ export function collectHardStopBlockers(state) {
164
+ const questions = state.questionLog ?? [];
165
+ const questionById = new Map(questions.map((question) => [question.id, question]));
166
+ const pendingQuestions = questions.filter((question) => question.status === "pending");
167
+ const pendingObligations = (state.questionObligations ?? []).filter((obligation) => obligation.status === "pending");
168
+ const blockers = [];
169
+ pendingQuestions.forEach((question, index) => {
170
+ const hardStop = inferQuestionHardStopScope(question);
171
+ if (!hardStop) {
172
+ return;
173
+ }
174
+ blockers.push(createBlocker({
175
+ type: "question",
176
+ id: question.id,
177
+ scope: hardStop.scope,
178
+ prompt: question.prompt.question,
179
+ reason: question.prompt.displayReason ?? question.prompt.rationale?.[0] ?? "A Research Specification-affecting question is pending.",
180
+ sourceField: hardStop.sourceField,
181
+ priorityOffset: index / 1000
182
+ }));
183
+ });
184
+ pendingObligations.forEach((obligation, index) => {
185
+ const linkedQuestion = obligation.questionId ? questionById.get(obligation.questionId) : undefined;
186
+ const hardStop = inferObligationHardStopScope(obligation, linkedQuestion);
187
+ if (!hardStop) {
188
+ return;
189
+ }
190
+ blockers.push(createBlocker({
191
+ type: "obligation",
192
+ id: obligation.id,
193
+ scope: hardStop.scope,
194
+ prompt: obligation.prompt,
195
+ reason: obligation.reason,
196
+ sourceField: hardStop.sourceField,
197
+ questionId: obligation.questionId,
198
+ priorityOffset: 0.1 + index / 1000
199
+ }));
200
+ });
201
+ for (const [index, patch] of (state.specPatches ?? []).entries()) {
202
+ if (patch.status !== "proposed" || patch.decisionRecordId) {
203
+ continue;
204
+ }
205
+ const scope = patch.changes.map(scopeFromPatchChange).find((candidate) => Boolean(candidate));
206
+ if (!scope) {
207
+ continue;
208
+ }
209
+ blockers.push(createBlocker({
210
+ type: "spec_patch",
211
+ id: patch.id,
212
+ scope,
213
+ prompt: patch.title,
214
+ reason: patch.rationale ?? "A proposed Research Specification patch changes protected research state.",
215
+ sourceField: "ResearchSpecificationPatch.changes",
216
+ questionId: patch.questionRecordId,
217
+ priorityOffset: 0.2 + index / 1000
218
+ }));
219
+ }
220
+ const byKey = new Map();
221
+ for (const blocker of blockers) {
222
+ byKey.set(`${blocker.type}:${blocker.id}`, blocker);
223
+ }
224
+ const activeBlockers = [...byKey.values()].sort((left, right) => left.priority - right.priority ||
225
+ left.type.localeCompare(right.type) ||
226
+ left.id.localeCompare(right.id));
227
+ const blockerQuestionIds = new Set(activeBlockers.filter((blocker) => blocker.type === "question").map((blocker) => blocker.id));
228
+ const blockerObligationIds = new Set(activeBlockers.filter((blocker) => blocker.type === "obligation").map((blocker) => blocker.id));
229
+ const stalePendingQuestionCount = pendingQuestions.filter((question) => !blockerQuestionIds.has(question.id)).length;
230
+ const stalePendingObligationCount = pendingObligations.filter((obligation) => !blockerObligationIds.has(obligation.id)).length;
231
+ return {
232
+ stopWouldBlock: activeBlockers.length > 0,
233
+ activeBlockers,
234
+ staleOrUnrelatedPendingQuestionCount: stalePendingQuestionCount,
235
+ stalePendingQuestionCount,
236
+ stalePendingObligationCount,
237
+ nextActions: activeBlockers.length > 0
238
+ ? activeBlockers.slice(0, 3).map((blocker) => blocker.commandHint)
239
+ : stalePendingQuestionCount > 0 || stalePendingObligationCount > 0
240
+ ? ["Review stale non-hard-stop pending LongTable questions when convenient; they do not block Stop."]
241
+ : []
242
+ };
243
+ }
package/dist/index.d.ts CHANGED
@@ -1 +1,2 @@
1
1
  export * from "./types.js";
2
+ export * from "./hard-stop.js";
package/dist/index.js CHANGED
@@ -1 +1,2 @@
1
1
  export * from "./types.js";
2
+ export * from "./hard-stop.js";
package/dist/types.d.ts CHANGED
@@ -9,8 +9,8 @@ export type NarrativeTraceVisibility = "explicit" | "inferred";
9
9
  export type HypothesisStatus = "unconfirmed" | "confirmed" | "rejected";
10
10
  export type ProviderKind = "claude" | "codex";
11
11
  export type RoleKey = string;
12
- export type InvocationKind = "single_role" | "panel" | "team" | "team_debate" | "status";
13
- export type InvocationSurface = "native_parallel" | "native_subagents" | "generated_skill" | "prompt_alias" | "sequential_fallback" | "file_backed_debate" | "mcp_transport";
12
+ export type InvocationKind = "single_role" | "panel" | "panel_debate" | "team" | "team_debate" | "status";
13
+ export type InvocationSurface = "native_parallel" | "native_subagents" | "generated_skill" | "prompt_alias" | "sequential_fallback" | "file_backed_panel_debate" | "file_backed_debate" | "mcp_transport";
14
14
  export type InvocationStatus = "planned" | "running" | "completed" | "blocked" | "degraded" | "error";
15
15
  export type InteractionDepth = "independent" | "cross_reviewed" | "debated";
16
16
  export type PanelVisibility = "synthesis_only" | "show_on_conflict" | "always_visible";
@@ -162,7 +162,7 @@ export interface TeamDebateRun {
162
162
  status: InvocationStatus;
163
163
  surface: InvocationSurface;
164
164
  interactionDepth: InteractionDepth;
165
- roundPolicy: "fixed" | "team_cross_review";
165
+ roundPolicy: "fixed" | "panel_cross_review" | "team_cross_review";
166
166
  roundCount: number;
167
167
  artifactRoot: string;
168
168
  rounds: TeamDebateRound[];
@@ -191,6 +191,7 @@ export interface InferredHypothesis {
191
191
  status: HypothesisStatus;
192
192
  }
193
193
  export type QuestionCommitmentFamily = "scope" | "construct" | "coding" | "method" | "evidence" | "epistemic_authority" | "product_policy";
194
+ export type QuestionHardStopScope = "research_question" | "scope" | "construct" | "method" | "evidence" | "protected_decision";
194
195
  export type QuestionEpistemicBasis = "researcher_knowledge" | "project_state" | "external_evidence" | "ai_inference" | "mixed";
195
196
  export interface DecisionRecord {
196
197
  id: string;
@@ -323,6 +324,7 @@ export interface RoleAuditResult {
323
324
  };
324
325
  }
325
326
  export type QuestionPromptType = "single_choice" | "multi_choice" | "free_text";
327
+ export type HardStopScope = QuestionHardStopScope;
326
328
  export interface QuestionPrompt {
327
329
  id: string;
328
330
  checkpointKey?: string;
@@ -360,6 +362,8 @@ export interface QuestionRecord {
360
362
  createdAt: string;
361
363
  updatedAt: string;
362
364
  status: QuestionRecordStatus;
365
+ hardStop?: boolean;
366
+ hardStopScope?: HardStopScope;
363
367
  commitmentFamily?: QuestionCommitmentFamily;
364
368
  epistemicBasis?: QuestionEpistemicBasis;
365
369
  prompt: QuestionPrompt;
@@ -495,6 +499,7 @@ export interface LongTableHookRun {
495
499
  }
496
500
  export type LongTableQuestionObligationKind = "required_question" | "first_research_shape_confirmation" | "research_specification_confirmation";
497
501
  export type LongTableQuestionObligationStatus = "pending" | "satisfied" | "cleared";
502
+ export type LongTableHardStopScope = HardStopScope;
498
503
  export interface LongTableQuestionObligation {
499
504
  id: string;
500
505
  kind: LongTableQuestionObligationKind;
@@ -506,6 +511,8 @@ export interface LongTableQuestionObligation {
506
511
  questionId?: string;
507
512
  decisionId?: string;
508
513
  sourceHookId?: string;
514
+ hardStop?: boolean;
515
+ hardStopScope?: HardStopScope;
509
516
  }
510
517
  export interface RuntimeGuidance {
511
518
  mode: InteractionMode;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@longtable/core",
3
- "version": "0.1.51",
3
+ "version": "0.1.53",
4
4
  "private": false,
5
5
  "description": "Provider-neutral domain models and contracts for LongTable",
6
6
  "type": "module",