@attentionform/astra-mcp-server 0.0.0-stage → 0.1.0

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,15 @@
1
+ export declare const MCP_RESEARCH_FLAG = "--with-research";
2
+ /** Legacy helper: reports argv intent only; server startup accepts no such flag. */
3
+ export declare function isResearchEnabled(argv?: readonly string[]): boolean;
4
+ /** Lean-6 tool names (locked, no single_op). */
5
+ export declare const LEAN6_TOOL_NAMES: readonly ["open_document", "inspect_document", "search_content", "execute_batch", "save_document", "close_document"];
6
+ export type Lean6ToolName = (typeof LEAN6_TOOL_NAMES)[number];
7
+ export declare function getLean6ToolNames(): Lean6ToolName[];
8
+ /** Git-history tool names (history browsing + preview + revert + redo; registered alongside lean-6). */
9
+ export declare const GIT_HISTORY_TOOL_NAMES: readonly ["get_document_history", "preview_revert", "revert_document", "redo_document"];
10
+ export type GitHistoryToolName = (typeof GIT_HISTORY_TOOL_NAMES)[number];
11
+ export declare function getGitHistoryToolNames(): GitHistoryToolName[];
12
+ /** Document-audit tool names (read-only preflight + census + scrub + canonical repair + file ingest). */
13
+ export declare const PARITY_TOOL_NAMES: readonly ["validate_document", "get_stats", "sanitize_document", "normalize_document", "import_document"];
14
+ export type ParityToolName = (typeof PARITY_TOOL_NAMES)[number];
15
+ export declare function getParityToolNames(): ParityToolName[];
@@ -0,0 +1,12 @@
1
+ export * from './mcp/normalize-read';
2
+ export * from './mcp/tool-names';
3
+ export * from './mcp/errors';
4
+ export * from './mcp/diff';
5
+ export * from './mcp/descriptions';
6
+ export * from './mcp/schemas';
7
+ export * from './mcp/handlers/open-inspect';
8
+ export * from './mcp/handlers/search-batch';
9
+ export * from './mcp/handlers/save-close';
10
+ export * from './mcp/handlers/history';
11
+ export * from './mcp/handlers/export-audit';
12
+ export * from './mcp/registry';
@@ -0,0 +1,422 @@
1
+ import { type DocumentAST, type DocxParserMacroOptions, DocxParser, OperationEngine, ToonGenerator, canonicalizeForAsset as canonicalizeForMcp, type StyleGuideConfig } from '@attentionform/astra-engine';
2
+ /**
3
+ * Sprint-2 Muse session store (MCP Lean-6 backing).
4
+ *
5
+ * Sessions: Map<sessionId,DocHandle> (multi-doc; replaces the single active-document holder).
6
+ * - LRU 10 docs, 200MB total cap, idle 30min evict.
7
+ * - Per-session revision (bumped on every successful execute_batch).
8
+ * - Per-session mutex (each DocHandle owns its lock; no shared global lock).
9
+ *
10
+ * There is intentionally no shared module-level instance here:
11
+ * callers construct their own store via createMcpSessionStore().
12
+ */
13
+ export declare const MCP_SESSION_MAX_DOCS = 10;
14
+ export declare const MCP_SESSION_MAX_BYTES: number;
15
+ export declare const MCP_SESSION_IDLE_MS: number;
16
+ /**
17
+ * Redo-stack quotas (per session, in-memory). High defaults so normal
18
+ * revert/redo chains (1-5 entries, KBs) never hit them; only runaway
19
+ * rapid-revert loops do. Configurable via store options or env.
20
+ */
21
+ export declare const SESSION_REDO_MAX_DEPTH = 100;
22
+ export declare const SESSION_REDO_MAX_BYTES: number;
23
+ /** Env overrides for the redo quotas (positive integers, bytes for the latter). */
24
+ export declare const SESSION_REDO_MAX_DEPTH_ENV = "ASTRA_REDO_MAX_DEPTH";
25
+ export declare const SESSION_REDO_MAX_BYTES_ENV = "ASTRA_REDO_MAX_BYTES";
26
+ /** L1 upper clamps: env/explicit values above these are capped (env-driven memory exhaustion). */
27
+ export declare const SESSION_REDO_MAX_DEPTH_CLAMP = 10000;
28
+ export declare const SESSION_REDO_MAX_BYTES_CLAMP: number;
29
+ /** Error code for quota refusals (shared with history sidecar caps). */
30
+ export declare const SESSION_QUOTA_CODE: "E_QUOTA";
31
+ /** Prefix for every server-minted session id (`sess_<uuid>`). */
32
+ export declare const MCP_SESSION_ID_PREFIX = "sess_";
33
+ /** CLI flag enabling deterministic client-supplied ids (tests only, default off). */
34
+ export declare const CLIENT_SESSION_ID_FLAG = "--allow-client-session-ids";
35
+ /** Env var mirroring the flag (accepted values: 1/true, case-insensitive). */
36
+ export declare const CLIENT_SESSION_ID_ENV = "ASTRA_ALLOW_CLIENT_SESSION_IDS";
37
+ export interface McpSessionStoreOptions {
38
+ maxDocs?: number;
39
+ maxBytes?: number;
40
+ idleMs?: number;
41
+ /** Redo-stack depth cap (entries). Defaults to SESSION_REDO_MAX_DEPTH / env. */
42
+ redoMaxDepth?: number;
43
+ /** Redo-stack bytes cap (sum of snapshot bytes). Defaults to SESSION_REDO_MAX_BYTES / env. */
44
+ redoMaxBytes?: number;
45
+ /**
46
+ * Hatch for deterministic ids in tests. Default off: client-supplied
47
+ * sessionIds are rejected with E_VALIDATION and every open mints
48
+ * `sess_<uuid>`. Also enabled via --allow-client-session-ids /
49
+ * ASTRA_ALLOW_CLIENT_SESSION_IDS.
50
+ */
51
+ allowClientSessionIds?: boolean;
52
+ }
53
+ /** True when the deterministic-id hatch is enabled via env. */
54
+ export declare function isClientSessionIdHatchEnabledByEnv(env?: NodeJS.ProcessEnv): boolean;
55
+ /** True when argv carries --allow-client-session-ids before any `--` marker. */
56
+ export declare function hasClientSessionIdHatch(argv?: readonly string[]): boolean;
57
+ /** Client-facing rejection for a squatted sessionId (E_VALIDATION + remedy). */
58
+ export declare function buildClientSessionIdRejection(tool: string, raw: unknown): {
59
+ code: 'E_VALIDATION';
60
+ message: string;
61
+ remedy: string;
62
+ };
63
+ /** Resolves the redo depth cap (explicit > env > default). Never below 1. L1: clamped to SESSION_REDO_MAX_DEPTH_CLAMP. */
64
+ export declare function resolveRedoMaxDepth(explicit?: number, env?: NodeJS.ProcessEnv): number;
65
+ /** Resolves the redo bytes cap (explicit > env > default). Never below 1. L1: clamped to SESSION_REDO_MAX_BYTES_CLAMP. */
66
+ export declare function resolveRedoMaxBytes(explicit?: number, env?: NodeJS.ProcessEnv): number;
67
+ /** Byte size of one package snapshot (sum of part lengths). */
68
+ export declare function redoSnapshotByteSize(snap: Map<string, Buffer>): number;
69
+ /** Per-session async mutex (promise-chain, no global lock). */
70
+ export declare class SessionMutex {
71
+ private tail;
72
+ private inflight;
73
+ runExclusive<T>(fn: () => Promise<T>): Promise<T>;
74
+ /**
75
+ * M1: real locked state (queued + running).
76
+ * True while any runExclusive section is queued or holding the lock.
77
+ * Used by closeDocument to fail closed with E_CONFLICT instead of racing
78
+ * a concurrent batch.
79
+ */
80
+ isLocked(): boolean;
81
+ }
82
+ export interface DocHandle {
83
+ sessionId: string;
84
+ filePath: string;
85
+ parser: DocxParser;
86
+ ast: DocumentAST;
87
+ engine: OperationEngine;
88
+ toon: ToonGenerator;
89
+ revision: number;
90
+ /**
91
+ * Phase C: revision at the last successful save_document. dirty =
92
+ * revision !== savedRevision (in-memory session holds unsaved
93
+ * execute_batch edits vs the last file written to disk). Set to 1 on
94
+ * open, re-synced on every successful save.
95
+ */
96
+ savedRevision: number;
97
+ byteSize: number;
98
+ lastAccess: number;
99
+ mutex: SessionMutex;
100
+ /**
101
+ * History redo stack: full package snapshots captured before each
102
+ * revert_document restore (newest last). redo_document pops one entry,
103
+ * restores it, and commits it as a new head. Cleared by the next
104
+ * successful execute_batch (a fresh edit invalidates the redo chain) and
105
+ * dropped with the session on close/evict.
106
+ *
107
+ * R-2 exactness: entries are immutable pristine copies (the stack
108
+ * deep-copies every pushed map, so later live-package mutations can never
109
+ * corrupt a stored source), each bound to the sha256 fingerprint recorded
110
+ * at push time. A redo must restore EXACTLY the stored pre-revert bytes or
111
+ * fail with E_STATE (see assertRedoSnapshotExact) — never a third mixed
112
+ * state. The stack stays Array-compatible (push/pop/index/length) so the
113
+ * existing session-boundary semantics (cleared on fresh edit, dies with
114
+ * the session) are unchanged.
115
+ */
116
+ redoStack: SessionRedoStack;
117
+ /**
118
+ * Audit depth (in-memory, per-session): operations + touched node ids
119
+ * accumulated since the last successful save/revert/redo. Consumed at the
120
+ * next history commit to fill the entry's op count + affected node ids,
121
+ * then reset by markSaved. Best-effort and bounded (never fails a batch);
122
+ * it dies with the session and is NOT durable history (see the HOLD note
123
+ * on export_audit_trail: .astra/ itself is deletable local state).
124
+ */
125
+ pendingAudit: {
126
+ ops: number;
127
+ nodeIds: string[];
128
+ };
129
+ /**
130
+ * Attached style guide ("aesthetic skin"): set at session creation via
131
+ * new_document/compose_document `styleGuide`, persists for the session,
132
+ * mirrored onto `engine` so batches inherit it when they omit their own.
133
+ * Per-batch `styleGuide` overrides apply to that batch only (restored
134
+ * after). No detach API — open a new session to change guides.
135
+ */
136
+ styleGuide?: StyleGuideConfig;
137
+ /** Raw guide ref the session guide was resolved from (name or file path). */
138
+ styleGuideRef?: string;
139
+ }
140
+ /**
141
+ * Per-commit audit metadata recorded alongside each history commit.
142
+ * Keyed by commit oid in a store-level map (in-memory only: re-opens in a
143
+ * fresh store lose pre-existing entries, and deleting .astra/ wipes the
144
+ * underlying history itself — see the HOLD retention note).
145
+ */
146
+ export interface CommitAuditMeta {
147
+ author: string;
148
+ /** Wall-clock epoch millis at commit time (git stamps seconds only). */
149
+ timestampMs: number;
150
+ /** Operations folded into this commit (saves: since last save; reverts: steps back; redos: 1). */
151
+ opCount: number;
152
+ /** Node ids touched by this commit (bounded, document order, best-effort). */
153
+ affectedNodeIds: string[];
154
+ }
155
+ /** Cap on affected node ids retained per commit (bounded envelope). */
156
+ export declare const COMMIT_AUDIT_NODE_IDS_MAX = 100;
157
+ /** Cap on commit-meta entries retained per store (bounded ring, oldest dropped). */
158
+ export declare const COMMIT_AUDIT_META_MAX = 2000;
159
+ /** Error code for exact-state violations (redo/restore source diverged). */
160
+ export declare const REDO_EXACTNESS_CODE: "E_STATE";
161
+ /** sha256 hex over one package-part buffer (redo integrity binding). */
162
+ export declare function hashRedoBytes(buf: Buffer): string;
163
+ /** Integrity fingerprint of one full package snapshot (stable key order). */
164
+ export interface RedoSnapshotIntegrity {
165
+ /** sha256 over word/document.xml bytes ('' when the part is absent). */
166
+ docXmlHash: string;
167
+ /** sha256 over every entry (name + bytes, sorted by name). */
168
+ packageHash: string;
169
+ /** Sorted entry names captured in the snapshot. */
170
+ entryNames: string[];
171
+ /** Total snapshot bytes across all entries. */
172
+ byteSize: number;
173
+ }
174
+ /** Deep-copies a package snapshot (new map, copied buffers). */
175
+ export declare function clonePackageSnapshot(snap: Map<string, Buffer>): Map<string, Buffer>;
176
+ /** Fingerprints a package snapshot without mutating it. */
177
+ export declare function fingerprintPackageSnapshot(snap: Map<string, Buffer>): RedoSnapshotIntegrity;
178
+ /**
179
+ * R-2 session-bound redo stack.
180
+ *
181
+ * Array-compatible (push/pop/index/length behave like the plain array the
182
+ * callers already use, including `length = 0` truncation to invalidate the
183
+ * chain), with two hardening guarantees on top:
184
+ * - push() stores an immutable pristine copy (later live-package mutations
185
+ * can never corrupt the redo source by aliasing);
186
+ * - every stored copy is bound to the fingerprint recorded at push time,
187
+ * retrievable via integrityOf() for exactness proofs.
188
+ *
189
+ * Integrity metadata lives in a WeakMap keyed by the stored copy, so
190
+ * `length = 0` truncation or close/evict simply orphans the metadata (no
191
+ * parallel-array bookkeeping to go stale) and entries are never shared
192
+ * back out for mutation.
193
+ */
194
+ export declare class SessionRedoStack extends Array<Map<string, Buffer>> {
195
+ private readonly integrity;
196
+ private readonly maxDepth;
197
+ private readonly maxBytes;
198
+ constructor(opts?: {
199
+ maxDepth?: number;
200
+ maxBytes?: number;
201
+ });
202
+ getMaxDepth(): number;
203
+ getMaxBytes(): number;
204
+ /** Sum of stored snapshot bytes (recomputed so pop/length-truncation stay exact). */
205
+ currentBytes(): number;
206
+ push(...items: Array<Map<string, Buffer>>): number;
207
+ /** Newest entry without consuming (undefined when the stack is empty). */
208
+ peek(): Map<string, Buffer> | undefined;
209
+ /**
210
+ * Push-time fingerprint of a stored entry (undefined for foreign maps
211
+ * that bypassed push, e.g. entries built by hand in tests).
212
+ */
213
+ integrityOf(entry: Map<string, Buffer>): RedoSnapshotIntegrity | undefined;
214
+ }
215
+ /**
216
+ * R-2 exact-or-error check: asserts that a restored live snapshot is
217
+ * byte-identical to the redo source it must equal (pre-revert on success,
218
+ * post-revert when the restore was rolled back). Throws E_STATE with a
219
+ * remedy naming the divergent parts when exact restore is impossible, so
220
+ * callers fail loudly instead of serving a third mixed state.
221
+ */
222
+ export declare function assertRedoSnapshotExact(expected: Map<string, Buffer>, actual: Map<string, Buffer>, context: string): void;
223
+ export interface McpSessionStats {
224
+ sessions: number;
225
+ totalBytes: number;
226
+ maxDocs: number;
227
+ maxBytes: number;
228
+ idleMs: number;
229
+ }
230
+ /**
231
+ * M3: workspace jail for MCP document paths.
232
+ * Mirrors the narrow sandbox (cwd/documents, tmpdir, app Drafts, extra roots)
233
+ * without importing the template-palette owner's modules (zero overlap).
234
+ * Every candidate is canonicalized (symlinks/junctions resolved, with
235
+ * nearest-existing-ancestor fallback for not-yet-created save targets) and
236
+ * containment is checked on the CANONICAL path.
237
+ *
238
+ * H2/D3: `ASTRA_ALLOWED_DIR` is NOT honored here (dropped: roots come only
239
+ * from explicit `--allowed-dir` flags, which the server entrypoint folds into
240
+ * `ASTRA_EXTRA_DOC_ROOTS`, plus the fixed base roots below). Any
241
+ * `ASTRA_ALLOWED_DIR` value in the environment is ignored.
242
+ * H2: delimiter-split `ASTRA_EXTRA_DOC_ROOTS` entries are existence-checked
243
+ * (must resolve to an existing directory) before joining the jail — phantom
244
+ * entries never widen anything. Primitives (`isMcpPathInsideDir`,
245
+ * `canonicalizeForMcp`) are the Q0a shared module (`@attentionform/astra-engine`
246
+ * sandbox/paths), so MCP/engine verdicts can never disagree on semantics.
247
+ */
248
+ export declare function getMcpAllowedRoots(): string[];
249
+ export { canonicalizeForMcp };
250
+ /**
251
+ * M3: jailed document-path validator.
252
+ * Returns the resolved absolute path. Throws E_SANDBOX (with remedy) on:
253
+ * non-string/empty, null byte, line break, `..` traversal, hidden segments,
254
+ * or canonical location outside the workspace roots. Throws E_VALIDATION on
255
+ * non-.docx extension. Symlinks/junctions are resolved BEFORE containment.
256
+ */
257
+ export declare function validateDocPath(rawPath: unknown): string;
258
+ /**
259
+ * M3: save-target validator (same jail as open).
260
+ * Save targets that don't exist yet are canonicalized via their nearest
261
+ * existing ancestor, so a symlinked parent dir pointing outside still fails.
262
+ */
263
+ export declare function validateSaveTarget(rawPath: unknown): string;
264
+ /**
265
+ * L4: openBuffer label validator (jails the `.astra` git sidecar).
266
+ * Absolute labels go through the full `validateDocPath` jail (E_SANDBOX on
267
+ * escape, E_VALIDATION on non-.docx) so `/etc/x.docx` can never become a
268
+ * sidecar write at `/etc/.astra/x.docx`. Relative memory labels are
269
+ * sanitized to a bare `.docx` basename (`memory.docx` fallback) and never
270
+ * produce an absolute sidecar path (git init skips non-absolute handles).
271
+ */
272
+ export declare function validateOpenBufferLabel(rawLabel: unknown): string;
273
+ /**
274
+ * Collects the versioned package extras (media/rels/styles/headers) for git.
275
+ * document.xml is committed separately; everything matching
276
+ * isTrackedPackageFile is snapshotted alongside it.
277
+ */
278
+ export declare function collectTrackedExtrasForGit(parser: DocxParser): Record<string, Buffer>;
279
+ /**
280
+ * Ensures the sidecar git repo exists for `docPath`, creating it with the
281
+ * current package state as the initial baseline when absent.
282
+ * Returns true when the repo already existed, false when freshly created.
283
+ * Best-effort at open (callers swallow failures so open semantics never
284
+ * change); hard requirement before commitChange at save/revert/redo.
285
+ */
286
+ export declare function ensureGitRepo(docPath: string, xml: string, extras: Record<string, Buffer>): Promise<boolean>;
287
+ /** Outcome of McpSessionStore.closeDocument (dirty state read under the delete lock). */
288
+ export interface CloseDocumentOutcome {
289
+ closed: boolean;
290
+ /** True when the dropped session held unsaved edits (discarded with the close). */
291
+ wasDirty: boolean;
292
+ /** Pending revisions discarded with the close (0 when clean). */
293
+ discardedRevisions: number;
294
+ }
295
+ export declare class McpSessionStore {
296
+ private readonly sessions;
297
+ private readonly maxDocs;
298
+ private readonly maxBytes;
299
+ private readonly idleMs;
300
+ private readonly allowClientSessionIds;
301
+ private readonly redoMaxDepth;
302
+ private readonly redoMaxBytes;
303
+ /**
304
+ * Phase C-4: ring of sessionIds dropped by eviction (LRU/byte-cap/idle),
305
+ * so later unknown-session failures can report a capacity-specific
306
+ * E_SESSION remedy instead of a generic unknown-id error. Explicit
307
+ * close_document deletes are NOT recorded here. Bounded (last 200).
308
+ */
309
+ private readonly evictionHistory;
310
+ /** Ids evicted by the most recent open (drained by handleOpenDocument for `evicted[]`). */
311
+ private lastOpenEvicted;
312
+ /**
313
+ * Store-level commit audit metadata (oid -> meta). In-memory only: fresh
314
+ * stores start empty and .astra/ deletion wipes the history it annotates.
315
+ */
316
+ private readonly commitMeta;
317
+ constructor(opts?: McpSessionStoreOptions);
318
+ getAllowClientSessionIds(): boolean;
319
+ getMaxDocs(): number;
320
+ getMaxBytes(): number;
321
+ getIdleMs(): number;
322
+ getRedoMaxDepth(): number;
323
+ getRedoMaxBytes(): number;
324
+ /** Fresh redo stack bound to this store's quotas. */
325
+ createRedoStack(): SessionRedoStack;
326
+ size(): number;
327
+ totalBytes(): number;
328
+ getStats(): McpSessionStats;
329
+ listSessionIds(): string[];
330
+ /** Evicts sessions idle longer than idleMs. Locked/dirty sessions are refused (never dropped). Returns evicted ids. */
331
+ evictExpired(now?: number): string[];
332
+ /** Phase C-4: record evicted ids (bounded ring) for capacity-aware remedies. */
333
+ private recordEvicted;
334
+ /** Phase C-4: true when `id` was dropped by eviction (not explicit close). */
335
+ wasEvicted(sessionId: string): boolean;
336
+ /**
337
+ * Phase C-4: ids evicted by the most recent openDocument/openBuffer call.
338
+ * Drained (cleared) on read so each open reports only its own evictions.
339
+ */
340
+ drainLastOpenEvicted(): string[];
341
+ /** Phase C-1b: true when the in-memory session holds unsaved edits. */
342
+ isDirty(handle: DocHandle): boolean;
343
+ /** Phase C-1b: re-syncs the saved marker after a successful save_document. */
344
+ markSaved(handle: DocHandle): void;
345
+ /**
346
+ * Audit depth: appends one batch's footprint to the since-save accumulator.
347
+ * Dedupes ids, preserves first-seen order, caps the list (best-effort:
348
+ * never throws, so a batch can never fail on audit bookkeeping).
349
+ */
350
+ accumulatePendingAudit(handle: DocHandle, appliedCount: number, nodeIds: readonly string[]): void;
351
+ /** Clears the since-save audit accumulator (called by markSaved). */
352
+ resetPendingAudit(handle: DocHandle): void;
353
+ /** Records per-commit audit metadata (bounded ring; oldest entries dropped). */
354
+ recordCommitMeta(oid: string, meta: CommitAuditMeta): void;
355
+ /** Reads per-commit audit metadata (undefined for commits made before tracking). */
356
+ getCommitMeta(oid: string): CommitAuditMeta | undefined;
357
+ /** LRU + byte-cap eviction (oldest insertion first). Never evicts `protectId`, locked, or dirty sessions. Returns evicted ids. */
358
+ private evictToFit;
359
+ private touch;
360
+ private newSessionId;
361
+ /** Mints an id no live session holds (retries on the near-impossible clash). */
362
+ private mintUniqueSessionId;
363
+ /**
364
+ * Resolves the session id for an open: omitted/blank mints a fresh
365
+ * `sess_<uuid>`; a supplied id is rejected with E_VALIDATION unless the
366
+ * deterministic-id hatch is on, and an id already held by a live session
367
+ * fails with E_CONFLICT — the live entry is never replaced.
368
+ */
369
+ private resolveOpenSessionId;
370
+ /**
371
+ * Opens a .docx file into a new (or reused) session.
372
+ * Revision starts at 1. LRU/byte/idle eviction runs before insert.
373
+ * Macro/active content refuses by default (E_VALIDATION naming the parts);
374
+ * pass {allowMacros:true} to preserve or {stripMacros:true} to strip.
375
+ */
376
+ openDocument(filePath: string, sessionId?: string, macroOpts?: DocxParserMacroOptions): Promise<DocHandle>;
377
+ /**
378
+ * Opens a document from an in-memory buffer (tests / headless flows).
379
+ * `labelPath` is the display path recorded on the handle. L4: absolute
380
+ * labels are jailed through validateDocPath (same jail as open) so a
381
+ * future production caller cannot turn the `.astra` git sidecar into a
382
+ * write primitive outside the jail (e.g. label `/etc/x.docx` → sidecar
383
+ * `/etc/.astra/x.docx`). Relative memory labels (e.g. `memory.docx`) are
384
+ * sanitized to a bare basename and never touch git (init skips non-absolute
385
+ * paths). Macro/active content refuses by default; pass {allowMacros:true}
386
+ * or {stripMacros:true}.
387
+ */
388
+ openBuffer(buf: Buffer, labelPath?: string, sessionId?: string, macroOpts?: DocxParserMacroOptions): Promise<DocHandle>;
389
+ /** Returns the live handle (refreshes LRU). Throws E_SESSION with remedy when missing/expired. */
390
+ get(sessionId: string): DocHandle;
391
+ has(sessionId: string): boolean;
392
+ /**
393
+ * Runs `fn` under the handle's own mutex (per-session, never global).
394
+ * Serializes concurrent execute_batch/save races on the same session while
395
+ * leaving other sessions fully parallel.
396
+ * H2: re-validates liveness AFTER acquiring the lock — a concurrent
397
+ * closeDocument that won the race deletes first, and this call fails with
398
+ * E_CONFLICT (not use-after-close).
399
+ */
400
+ withSession<T>(sessionId: string, fn: (handle: DocHandle) => Promise<T>): Promise<T>;
401
+ /** Bumps the per-session revision after a successful mutation. */
402
+ bumpRevision(handle: DocHandle): number;
403
+ /** Best-effort byte-size refresh after mutations (drives the 200MB cap). */
404
+ refreshByteSize(handle: DocHandle): number;
405
+ /**
406
+ * H2: lock-aware close. Acquires the SAME per-session SessionMutex instead
407
+ * of deleting without a lock (which raced withSession's critical section).
408
+ * - Unknown id -> { closed: false } (already closed/expired; adapter maps to E_SESSION).
409
+ * - Session busy (batch/save/inspect holding the lock) -> throws E_CONFLICT
410
+ * with remedy (caller retries after the in-flight op finishes).
411
+ * - Otherwise the delete itself runs under the mutex, so a withSession that
412
+ * queued between the busy-check and the delete either sees the live handle
413
+ * (if close lost) or fails its post-lock liveness check (if close won).
414
+ * The outcome names the dropped dirty state (wasDirty + discardedRevisions,
415
+ * read under the same lock as the delete) so callers never silently lose
416
+ * unsaved edits: a dirty close warns instead of vanishing quietly.
417
+ */
418
+ closeDocument(sessionId: string): Promise<CloseDocumentOutcome>;
419
+ clear(): void;
420
+ }
421
+ /** Factory (no singleton): each server/test constructs its own store. */
422
+ export declare function createMcpSessionStore(opts?: McpSessionStoreOptions): McpSessionStore;
@@ -0,0 +1,124 @@
1
+ import { z } from 'zod';
2
+ import { type EngineErrorCode, type ResolvedStyleGuide, type StyleGuideConfig, type StyleGuideSummary } from '@attentionform/astra-engine';
3
+ import { type DocHandle, type McpSessionStore } from '../lib/session-store';
4
+ import { type BatchOpCreatedIds } from '../lib/mcp-tool-adapter';
5
+ /** Upper bound on the Markdown source per call (chars). Larger sources should be split by the caller. */
6
+ export declare const COMPOSE_MARKDOWN_MAX = 500000;
7
+ export interface McpGuideJail {
8
+ /**
9
+ * Explicit jail directory (already validated against the MCP roots, e.g.
10
+ * the batch/compose `allowedDir`). Wins over the session anchor.
11
+ */
12
+ allowedDir?: string;
13
+ /**
14
+ * Session filesystem anchor (absolute open-document path): file guide
15
+ * refs resolve under its directory (same trust as batch image reads).
16
+ */
17
+ sessionFilePath?: string;
18
+ }
19
+ /**
20
+ * Resolves a `styleGuide` ref under the MCP jail. Named guides resolve to
21
+ * bundled files (unknown names fail closed E_VALIDATION naming the
22
+ * available set); file refs resolve under the explicit `allowedDir` else
23
+ * the session anchor directory; remote URLs fail closed. A file-backed ref
24
+ * with no anchor (in-memory session, no allowedDir) fails E_SANDBOX —
25
+ * same rule as batch image `filePath` (never silently cwd-anchored).
26
+ */
27
+ export declare function resolveMcpStyleGuide(ref: string, jail?: McpGuideJail): ResolvedStyleGuide;
28
+ /**
29
+ * Attaches a resolved guide to a session (persists for the session,
30
+ * mirrored onto the session engine so later batches inherit it).
31
+ */
32
+ export declare function attachSessionStyleGuide(handle: DocHandle, ref: string, resolved: ResolvedStyleGuide): StyleGuideConfig;
33
+ export declare const ComposeDocumentSchema: z.ZodObject<{
34
+ sessionId: z.ZodString;
35
+ markdown: z.ZodString;
36
+ allowedDir: z.ZodOptional<z.ZodString>;
37
+ template: z.ZodOptional<z.ZodEnum<{
38
+ report: "report";
39
+ memo: "memo";
40
+ blank: "blank";
41
+ }>>;
42
+ styleGuide: z.ZodOptional<z.ZodString>;
43
+ dollarMath: z.ZodOptional<z.ZodBoolean>;
44
+ }, z.core.$strict>;
45
+ export type ComposeDocumentInput = z.infer<typeof ComposeDocumentSchema>;
46
+ export declare const COMPOSE_DOCUMENT_DESCRIPTION: string;
47
+ type ComposeDocumentOk = {
48
+ ok: true;
49
+ sessionId: string;
50
+ /** Operations the source compiled to (before chunking). */
51
+ opCount: number;
52
+ appliedCount: number;
53
+ revision: number;
54
+ /** True when the in-memory session differs from the last saved file (always true after a compose). */
55
+ dirty: boolean;
56
+ /** New node ids created by this compose, in post-compose document order. */
57
+ createdNodeIds: string[];
58
+ /**
59
+ * Created paragraph ids (`md_0003`) mapped to the w14:paraId the saved file carries (what
60
+ * a reopened session reports). Absent when nothing mapped; tables have no paraId and are
61
+ * renumbered by position on reopen.
62
+ */
63
+ persistentIds?: Record<string, string>;
64
+ /** Per-op attribution (same rule as execute_batch: exact id claims, remainder best-effort). */
65
+ results: BatchOpCreatedIds[];
66
+ /** Read-only post-compose OOXML verdict (proof the composed package validates; save re-gates before writing). */
67
+ validation: {
68
+ valid: boolean;
69
+ errorCount: number;
70
+ errors: Array<{
71
+ description: string;
72
+ xPath?: string;
73
+ }>;
74
+ };
75
+ skippedIds: string[];
76
+ warnings: string[];
77
+ revisionsPending: number;
78
+ revisionsRemedy?: string;
79
+ possiblyStaleBodyRefs: {
80
+ nodeId: string;
81
+ preview: string;
82
+ }[];
83
+ normalized: string[];
84
+ /** True only when this compose dropped a non-empty redo stack (absent otherwise). */
85
+ redoCleared?: boolean;
86
+ /** Names the dropped redo depth (present only when redoCleared). */
87
+ redoClearedWarning?: string;
88
+ /** Compact summary of the guide this compose attached (absent when none attached). */
89
+ styleGuide?: StyleGuideSummary;
90
+ /** Parser diagnostics of the attached guide (present only when this compose attached one). */
91
+ styleGuideWarnings?: string[];
92
+ };
93
+ type ComposeDocumentErr = {
94
+ ok: false;
95
+ code: EngineErrorCode;
96
+ message: string;
97
+ remedy: string;
98
+ };
99
+ /**
100
+ * Q0b-gap: pure mapper for compose-stage failures (exported for the E-code
101
+ * contract test). Every engine code survives verbatim via the shared
102
+ * getEngineErrorCode table; E_VALIDATION stays the fallback for uncoded
103
+ * failures. Previously E_LIMIT/E_SECURITY/E_DEPENDENCY/E_AUTH collapsed to
104
+ * E_VALIDATION via the local KNOWN_COMPOSE_CODES allowlist.
105
+ */
106
+ export declare function composeErrorFrom(err: unknown, fallbackRemedy: string): ComposeDocumentErr;
107
+ /**
108
+ * Jail check for the asset directory: the resolved canonical location must
109
+ * sit inside one of the workspace roots. Returns the canonical absolute path.
110
+ * Throws E_SANDBOX (with remedy) when the directory escapes the roots.
111
+ */
112
+ export declare function assertPathAllowed(allowedDir: string, roots: string[]): string;
113
+ /**
114
+ * Compile Markdown and apply it to an open session through the engine compose
115
+ * path. The compile is pure (runs before the session lock, so input errors
116
+ * write nothing and touch no session); execution delegates to
117
+ * `composeDocument` on the session parser (chunked at the engine op cap with
118
+ * template-seed check, then inline-span run splitting), so the strict schema
119
+ * gate, quotas, structural checks and per-chunk atomic rollback all apply
120
+ * exactly as they do for execute_batch, plus spans. Never throws — every
121
+ * failure maps to {ok:false,code,remedy}.
122
+ */
123
+ export declare function handleComposeDocument(store: McpSessionStore, rawArgs: unknown): Promise<ComposeDocumentOk | ComposeDocumentErr>;
124
+ export {};
@@ -0,0 +1,43 @@
1
+ import { z } from 'zod';
2
+ import { McpSessionStore } from '../lib/session-store';
3
+ import { type PreflightIssue } from '@attentionform/astra-engine';
4
+ export declare const ExportDocumentSchema: z.ZodObject<{
5
+ sessionId: z.ZodString;
6
+ format: z.ZodEnum<{
7
+ markdown: "markdown";
8
+ html: "html";
9
+ md: "md";
10
+ pdf: "pdf";
11
+ }>;
12
+ outPath: z.ZodOptional<z.ZodString>;
13
+ tagged_pdf: z.ZodOptional<z.ZodBoolean>;
14
+ tagged: z.ZodOptional<z.ZodBoolean>;
15
+ strict: z.ZodOptional<z.ZodBoolean>;
16
+ }, z.core.$strip>;
17
+ export type ExportDocumentArgs = z.infer<typeof ExportDocumentSchema>;
18
+ export declare const EXPORT_DOCUMENT_DESCRIPTION: string;
19
+ /**
20
+ * Q0b: pure mapper for export-stage failures (exported for the E-code
21
+ * contract test). Every engine code survives verbatim via
22
+ * getEngineErrorCode; E_IO stays the fallback for uncoded failures.
23
+ */
24
+ export declare function mapExportError(err: unknown): {
25
+ code: string;
26
+ message: string;
27
+ remedy: string;
28
+ };
29
+ export declare function validateExportTarget(rawPath: unknown, expectedFormat: 'html' | 'markdown' | 'md' | 'pdf'): string;
30
+ export declare function handleExportDocument(store: McpSessionStore, rawArgs: unknown): Promise<{
31
+ ok: boolean;
32
+ format?: string;
33
+ path?: string;
34
+ bytes?: number;
35
+ content?: string;
36
+ dirty?: boolean;
37
+ revision?: number;
38
+ warnings?: PreflightIssue[];
39
+ blockers?: PreflightIssue[];
40
+ message?: string;
41
+ code?: string;
42
+ remedy?: string;
43
+ }>;
@@ -0,0 +1,53 @@
1
+ import { z } from 'zod';
2
+ import { type EngineErrorCode, type StyleGuideSummary } from '@attentionform/astra-engine';
3
+ import { type McpSessionStore } from '../lib/session-store';
4
+ /** Templates mirrored from the CLI `create` command (cli.ts CREATE_TEMPLATES). */
5
+ export declare const NEW_DOCUMENT_TEMPLATES: readonly ["report", "memo", "blank"];
6
+ export type NewDocumentTemplate = (typeof NEW_DOCUMENT_TEMPLATES)[number];
7
+ export declare const NewDocumentSchema: z.ZodObject<{
8
+ outPath: z.ZodString;
9
+ template: z.ZodOptional<z.ZodEnum<{
10
+ report: "report";
11
+ memo: "memo";
12
+ blank: "blank";
13
+ }>>;
14
+ force: z.ZodOptional<z.ZodBoolean>;
15
+ styleGuide: z.ZodOptional<z.ZodString>;
16
+ author: z.ZodOptional<z.ZodString>;
17
+ sessionId: z.ZodOptional<z.ZodString>;
18
+ }, z.core.$strict>;
19
+ export type NewDocumentInput = z.infer<typeof NewDocumentSchema>;
20
+ export declare const NEW_DOCUMENT_DESCRIPTION: string;
21
+ type NewDocumentOk = {
22
+ ok: true;
23
+ path: string;
24
+ sessionId: string;
25
+ revision: number;
26
+ template: NewDocumentTemplate;
27
+ bytes: number;
28
+ /** Compact summary of the guide attached at creation (absent when none). */
29
+ styleGuide?: StyleGuideSummary;
30
+ /** Parser diagnostics of the attached guide (present only when attached). */
31
+ styleGuideWarnings?: string[];
32
+ };
33
+ type NewDocumentErr = {
34
+ ok: false;
35
+ code: EngineErrorCode;
36
+ message: string;
37
+ remedy: string;
38
+ };
39
+ /**
40
+ * Q0b-gap: resolve the engine code verbatim via the shared table; fallback
41
+ * applies only when the error carries no known code (previously E_LIMIT/
42
+ * E_SECURITY/E_DEPENDENCY/E_AUTH and any other non-listed code collapsed to
43
+ * the per-site fallback). Exported for the E-code contract test.
44
+ */
45
+ export declare function resolveNewDocumentCode(err: unknown, fallback: EngineErrorCode): EngineErrorCode;
46
+ /**
47
+ * Create a template document on disk and open it into the session store.
48
+ * Mirrors CLI `create` semantics (template gate + validate-before-write);
49
+ * the write target is jailed to the MCP allowed roots (same jail as save).
50
+ * Never throws — every failure maps to {ok:false,code,remedy}.
51
+ */
52
+ export declare function handleNewDocument(store: McpSessionStore, rawArgs: unknown): Promise<NewDocumentOk | NewDocumentErr>;
53
+ export {};