@fenglimg/fabric-server 0.1.0 → 1.0.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.
package/dist/index.d.ts CHANGED
@@ -1,6 +1,15 @@
1
+ import { Server } from 'node:http';
1
2
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
3
 
3
4
  declare function createFabricServer(): McpServer;
4
5
  declare function startStdioServer(): Promise<void>;
6
+ declare function startHttpServer(options: {
7
+ port: number;
8
+ projectRoot: string;
9
+ host?: string;
10
+ authToken?: string;
11
+ dashboardDistPath?: string;
12
+ dev?: boolean;
13
+ }): Promise<Server>;
5
14
 
6
- export { createFabricServer, startStdioServer };
15
+ export { createFabricServer, startHttpServer, startStdioServer };
package/dist/index.js CHANGED
@@ -1,3 +1,13 @@
1
+ import {
2
+ FABRIC_DIR,
3
+ appendLedgerEntry,
4
+ atomicWriteText,
5
+ readAgentsMeta,
6
+ readHumanLock,
7
+ resolveProjectRoot,
8
+ sha256
9
+ } from "./chunk-KZO24GUQ.js";
10
+
1
11
  // src/index.ts
2
12
  import { resolve } from "path";
3
13
  import { fileURLToPath } from "url";
@@ -5,74 +15,29 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
15
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
16
 
7
17
  // src/tools/append-intent.ts
8
- import { appendFile } from "fs/promises";
9
- import { join as join2 } from "path";
10
- import { z as z2 } from "zod";
18
+ import { aiLedgerEntrySchema } from "@fenglimg/fabric-shared";
11
19
 
12
- // src/meta-reader.ts
13
- import { readFileSync } from "fs";
14
- import { join } from "path";
15
- import { z } from "zod";
16
- var agentsMetaNodeSchema = z.object({
17
- file: z.string(),
18
- scope_glob: z.string(),
19
- deps: z.array(z.string()),
20
- priority: z.enum(["high", "medium", "low"]),
21
- hash: z.string()
22
- });
23
- var agentsMetaSchema = z.object({
24
- revision: z.string(),
25
- nodes: z.record(agentsMetaNodeSchema)
26
- });
27
- var AgentsMetaFileMissingError = class extends Error {
28
- constructor(metaPath) {
29
- super(`Fabric agents metadata file is missing: ${metaPath}`);
30
- this.metaPath = metaPath;
31
- this.name = "AgentsMetaFileMissingError";
32
- }
33
- metaPath;
34
- code = "FABRIC_META_MISSING";
35
- };
36
- var AgentsMetaInvalidError = class extends Error {
37
- constructor(metaPath, cause) {
38
- const detail = cause instanceof Error ? cause.message : String(cause);
39
- super(`Fabric agents metadata file is invalid: ${metaPath}. ${detail}`);
40
- this.metaPath = metaPath;
41
- this.name = "AgentsMetaInvalidError";
42
- }
43
- metaPath;
44
- code = "FABRIC_META_INVALID";
45
- };
46
- function getAgentsMetaPath(projectRoot) {
47
- return join(projectRoot, ".fabric", "agents.meta.json");
48
- }
49
- function resolveProjectRoot() {
50
- return process.env.FABRIC_PROJECT_ROOT ?? process.cwd();
51
- }
52
- function readAgentsMeta(projectRoot) {
53
- const metaPath = getAgentsMetaPath(projectRoot);
54
- let raw;
55
- try {
56
- raw = readFileSync(metaPath, "utf8");
57
- } catch (error) {
58
- if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
59
- throw new AgentsMetaFileMissingError(metaPath);
60
- }
61
- throw error;
62
- }
63
- try {
64
- return agentsMetaSchema.parse(JSON.parse(raw));
65
- } catch (error) {
66
- throw new AgentsMetaInvalidError(metaPath, error);
67
- }
20
+ // src/services/append-intent.ts
21
+ async function appendIntent(projectRoot, input) {
22
+ const ts = Date.now();
23
+ const entry = await appendLedgerEntry(projectRoot, {
24
+ ...input.entry,
25
+ ts,
26
+ source: "ai"
27
+ });
28
+ return {
29
+ success: true,
30
+ timestamp: ts,
31
+ entry
32
+ };
68
33
  }
69
34
 
70
35
  // src/tools/append-intent.ts
71
36
  var inputSchema = {
72
- entry: z2.object({
73
- commit_sha: z2.string().optional(),
74
- intent: z2.string(),
75
- affected_paths: z2.array(z2.string())
37
+ entry: aiLedgerEntrySchema.omit({
38
+ id: true,
39
+ source: true,
40
+ ts: true
76
41
  })
77
42
  };
78
43
  function createTextResponse(payload) {
@@ -92,40 +57,68 @@ function registerAppendIntent(server) {
92
57
  inputSchema,
93
58
  async ({ entry }) => {
94
59
  const projectRoot = resolveProjectRoot();
95
- const ledgerPath = join2(projectRoot, ".intent-ledger.jsonl");
96
- const ts = Date.now();
97
- await appendFile(ledgerPath, `${JSON.stringify({ ts, ...entry })}
98
- `, "utf8");
99
- return createTextResponse({
100
- success: true,
101
- timestamp: ts
102
- });
60
+ const result = await appendIntent(projectRoot, { entry });
61
+ return createTextResponse(result);
103
62
  }
104
63
  );
105
64
  }
106
65
 
107
66
  // src/tools/get-rules.ts
67
+ import { z } from "zod";
68
+
69
+ // src/services/get-rules.ts
108
70
  import { readFile } from "fs/promises";
109
- import { join as join3 } from "path";
71
+ import { join } from "path";
110
72
  import { minimatch } from "minimatch";
111
- import { z as z3 } from "zod";
112
- var inputSchema2 = {
113
- path: z3.string().describe("Target file path to query rules for"),
114
- client_hash: z3.string().optional().describe("Revision hash from prior fab_get_rules response; enables stale detection")
115
- };
116
73
  var PRIORITY_ORDER = {
117
74
  high: 0,
118
75
  medium: 1,
119
76
  low: 2
120
77
  };
121
- function createTextResponse2(payload) {
122
- return {
123
- content: [
124
- {
125
- type: "text",
126
- text: JSON.stringify(payload)
78
+ async function getRules(projectRoot, input) {
79
+ const meta = readAgentsMeta(projectRoot);
80
+ const stale = input.client_hash !== void 0 && input.client_hash !== meta.revision;
81
+ const requestedPath = normalizePath(input.path);
82
+ const l0Content = await readFile(join(projectRoot, "AGENTS.md"), "utf8");
83
+ const matchedNodes = Object.entries(meta.nodes).filter(([, node]) => minimatch(requestedPath, normalizePath(node.scope_glob), { dot: true })).sort((left, right) => {
84
+ const [leftId, leftNode] = left;
85
+ const [rightId, rightNode] = right;
86
+ const priorityDelta = PRIORITY_ORDER[leftNode.priority] - PRIORITY_ORDER[rightNode.priority];
87
+ return priorityDelta !== 0 ? priorityDelta : leftId.localeCompare(rightId);
88
+ });
89
+ const loadedRules = await Promise.all(
90
+ matchedNodes.map(async ([nodeId, node]) => ({
91
+ level: classifyNode(nodeId),
92
+ entry: {
93
+ path: node.file,
94
+ content: await readFile(join(projectRoot, node.file), "utf8")
127
95
  }
128
- ]
96
+ }))
97
+ );
98
+ const l1 = [];
99
+ const l2 = [];
100
+ for (const rule of loadedRules) {
101
+ if (rule.level === "L1") {
102
+ l1.push(rule.entry);
103
+ continue;
104
+ }
105
+ if (rule.level === "L2") {
106
+ l2.push(rule.entry);
107
+ }
108
+ }
109
+ const humanLockedNearby = (await readHumanLock(projectRoot)).map((entry) => ({
110
+ file: entry.file,
111
+ excerpt: JSON.stringify(entry)
112
+ }));
113
+ return {
114
+ revision_hash: meta.revision,
115
+ stale,
116
+ rules: {
117
+ L0: l0Content,
118
+ L1: l1,
119
+ L2: l2,
120
+ human_locked_nearby: humanLockedNearby
121
+ }
129
122
  };
130
123
  }
131
124
  function normalizePath(value) {
@@ -140,32 +133,21 @@ function classifyNode(nodeId) {
140
133
  }
141
134
  return null;
142
135
  }
143
- async function readHumanLockedNearby(projectRoot) {
144
- const humanLockPath = join3(projectRoot, ".fabric", "human-lock.json");
145
- try {
146
- const raw = await readFile(humanLockPath, "utf8");
147
- const parsed = JSON.parse(raw);
148
- const entries = Array.isArray(parsed) ? parsed : parsed && typeof parsed === "object" && "human_locked" in parsed && Array.isArray(parsed.human_locked) ? parsed.human_locked : [];
149
- return entries.map((entry) => {
150
- if (!entry || typeof entry !== "object") {
151
- return {
152
- file: "(unknown)",
153
- excerpt: JSON.stringify(entry)
154
- };
136
+
137
+ // src/tools/get-rules.ts
138
+ var inputSchema2 = {
139
+ path: z.string().describe("Target file path to query rules for"),
140
+ client_hash: z.string().optional().describe("Revision hash from prior fab_get_rules response; enables stale detection")
141
+ };
142
+ function createTextResponse2(payload) {
143
+ return {
144
+ content: [
145
+ {
146
+ type: "text",
147
+ text: JSON.stringify(payload)
155
148
  }
156
- const file = typeof entry.file === "string" ? entry.file : "(unknown)";
157
- const excerptCandidate = typeof entry.excerpt === "string" ? entry.excerpt : typeof entry.locked_text === "string" ? entry.locked_text : typeof entry.text === "string" ? entry.text : typeof entry.content === "string" ? entry.content : JSON.stringify(entry);
158
- return {
159
- file,
160
- excerpt: excerptCandidate
161
- };
162
- });
163
- } catch (error) {
164
- if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
165
- return [];
166
- }
167
- throw error;
168
- }
149
+ ]
150
+ };
169
151
  }
170
152
  function registerGetRules(server) {
171
153
  server.tool(
@@ -174,87 +156,49 @@ function registerGetRules(server) {
174
156
  inputSchema2,
175
157
  async ({ path, client_hash }) => {
176
158
  const projectRoot = resolveProjectRoot();
177
- const meta = readAgentsMeta(projectRoot);
178
- const stale = client_hash !== void 0 && client_hash !== meta.revision;
179
- const requestedPath = normalizePath(path);
180
- const l0Content = await readFile(join3(projectRoot, "AGENTS.md"), "utf8");
181
- const matchedNodes = Object.entries(meta.nodes).filter(([, node]) => minimatch(requestedPath, normalizePath(node.scope_glob), { dot: true })).sort((left, right) => {
182
- const [leftId, leftNode] = left;
183
- const [rightId, rightNode] = right;
184
- const priorityDelta = PRIORITY_ORDER[leftNode.priority] - PRIORITY_ORDER[rightNode.priority];
185
- return priorityDelta !== 0 ? priorityDelta : leftId.localeCompare(rightId);
186
- });
187
- const loadedRules = await Promise.all(
188
- matchedNodes.map(async ([nodeId, node]) => ({
189
- level: classifyNode(nodeId),
190
- entry: {
191
- path: node.file,
192
- content: await readFile(join3(projectRoot, node.file), "utf8")
193
- }
194
- }))
195
- );
196
- const l1 = [];
197
- const l2 = [];
198
- for (const rule of loadedRules) {
199
- if (rule.level === "L1") {
200
- l1.push(rule.entry);
201
- continue;
202
- }
203
- if (rule.level === "L2") {
204
- l2.push(rule.entry);
205
- }
206
- }
207
- const humanLockedNearby = await readHumanLockedNearby(projectRoot);
208
- return createTextResponse2({
209
- revision_hash: meta.revision,
210
- stale,
211
- rules: {
212
- L0: l0Content,
213
- L1: l1,
214
- L2: l2,
215
- human_locked_nearby: humanLockedNearby
216
- }
217
- });
159
+ const result = await getRules(projectRoot, { path, client_hash });
160
+ return createTextResponse2(result);
218
161
  }
219
162
  );
220
163
  }
221
164
 
222
165
  // src/tools/update-registry.ts
223
- import { createHash } from "crypto";
224
- import { writeFile } from "fs/promises";
225
- import { join as join4 } from "path";
226
- import { z as z4 } from "zod";
227
- var inputSchema3 = {
228
- op: z4.enum(["add-node", "remove-node", "update-node"]),
229
- node_id: z4.string(),
230
- data: z4.record(z4.unknown()).optional()
231
- };
232
- var agentsMetaNodeSchema2 = z4.object({
233
- file: z4.string(),
234
- scope_glob: z4.string(),
235
- deps: z4.array(z4.string()),
236
- priority: z4.enum(["high", "medium", "low"]),
237
- hash: z4.string()
238
- });
239
- function createTextResponse3(payload) {
240
- return {
241
- content: [
166
+ import { z as z2 } from "zod";
167
+
168
+ // src/services/update-registry.ts
169
+ import { agentsMetaNodeSchema } from "@fenglimg/fabric-shared";
170
+ import { join as join2 } from "path";
171
+ async function updateRegistry(projectRoot, input) {
172
+ const metaPath = join2(projectRoot, FABRIC_DIR, "agents.meta.json");
173
+ const currentMeta = readAgentsMeta(projectRoot);
174
+ const nextMeta = applyRegistryOperation(currentMeta, input.op, input.node_id, input.data);
175
+ const newRevision = computeRevision(nextMeta);
176
+ await atomicWriteText(
177
+ metaPath,
178
+ `${JSON.stringify(
242
179
  {
243
- type: "text",
244
- text: JSON.stringify(payload)
245
- }
246
- ]
180
+ ...nextMeta,
181
+ revision: newRevision
182
+ },
183
+ null,
184
+ 2
185
+ )}
186
+ `
187
+ );
188
+ return {
189
+ revision_hash: newRevision,
190
+ success: true
247
191
  };
248
192
  }
249
193
  function computeRevision(meta) {
250
194
  const joinedHashes = Object.entries(meta.nodes).sort(([leftId], [rightId]) => leftId.localeCompare(rightId)).map(([, node]) => node.hash).join("");
251
- return `sha256:${createHash("sha256").update(joinedHashes).digest("hex")}`;
195
+ return sha256(joinedHashes);
252
196
  }
253
197
  function assertNodeData(data, message) {
254
198
  if (data === void 0) {
255
199
  throw new Error(message);
256
200
  }
257
- return agentsMetaNodeSchema2.parse(data);
201
+ return agentsMetaNodeSchema.parse(data);
258
202
  }
259
203
  function applyRegistryOperation(meta, op, nodeId, data) {
260
204
  const nextNodes = { ...meta.nodes };
@@ -276,7 +220,7 @@ function applyRegistryOperation(meta, op, nodeId, data) {
276
220
  if (currentNode === void 0) {
277
221
  throw new Error(`Cannot update missing Fabric registry node: ${nodeId}`);
278
222
  }
279
- nextNodes[nodeId] = agentsMetaNodeSchema2.parse({
223
+ nextNodes[nodeId] = agentsMetaNodeSchema.parse({
280
224
  ...currentNode,
281
225
  ...data
282
226
  });
@@ -285,6 +229,23 @@ function applyRegistryOperation(meta, op, nodeId, data) {
285
229
  nodes: nextNodes
286
230
  };
287
231
  }
232
+
233
+ // src/tools/update-registry.ts
234
+ var inputSchema3 = {
235
+ op: z2.enum(["add-node", "remove-node", "update-node"]),
236
+ node_id: z2.string(),
237
+ data: z2.record(z2.unknown()).optional()
238
+ };
239
+ function createTextResponse3(payload) {
240
+ return {
241
+ content: [
242
+ {
243
+ type: "text",
244
+ text: JSON.stringify(payload)
245
+ }
246
+ ]
247
+ };
248
+ }
288
249
  function registerUpdateRegistry(server) {
289
250
  server.tool(
290
251
  "fab_update_registry",
@@ -292,27 +253,8 @@ function registerUpdateRegistry(server) {
292
253
  inputSchema3,
293
254
  async ({ op, node_id, data }) => {
294
255
  const projectRoot = resolveProjectRoot();
295
- const metaPath = join4(projectRoot, ".fabric", "agents.meta.json");
296
- const currentMeta = readAgentsMeta(projectRoot);
297
- const nextMeta = applyRegistryOperation(currentMeta, op, node_id, data);
298
- const newRevision = computeRevision(nextMeta);
299
- await writeFile(
300
- metaPath,
301
- `${JSON.stringify(
302
- {
303
- ...nextMeta,
304
- revision: newRevision
305
- },
306
- null,
307
- 2
308
- )}
309
- `,
310
- "utf8"
311
- );
312
- return createTextResponse3({
313
- revision_hash: newRevision,
314
- success: true
315
- });
256
+ const result = await updateRegistry(projectRoot, { op, node_id, data });
257
+ return createTextResponse3(result);
316
258
  }
317
259
  );
318
260
  }
@@ -331,7 +273,7 @@ function formatError(error) {
331
273
  function createFabricServer() {
332
274
  const server = new McpServer({
333
275
  name: "fabric-context-server",
334
- version: "0.0.0"
276
+ version: "1.0.0"
335
277
  });
336
278
  registerGetRules(server);
337
279
  registerAppendIntent(server);
@@ -343,6 +285,20 @@ async function startStdioServer() {
343
285
  const transport = new StdioServerTransport();
344
286
  await server.connect(transport);
345
287
  }
288
+ async function startHttpServer(options) {
289
+ const { createFabricHttpApp } = await import("./http-3BNWQWPP.js");
290
+ const { port, projectRoot, host = "127.0.0.1", authToken, dashboardDistPath, dev } = options;
291
+ const app = createFabricHttpApp({ projectRoot, host, authToken, dashboardDistPath, dev });
292
+ return await new Promise((resolveServer, rejectServer) => {
293
+ const server = app.listen(port, host);
294
+ server.once("listening", () => {
295
+ resolveServer(server);
296
+ });
297
+ server.once("error", (error) => {
298
+ rejectServer(error);
299
+ });
300
+ });
301
+ }
346
302
  var entrypoint = process.argv[1];
347
303
  var currentFilePath = fileURLToPath(import.meta.url);
348
304
  var isMainModule = entrypoint !== void 0 && resolve(entrypoint) === currentFilePath;
@@ -354,5 +310,6 @@ if (isMainModule) {
354
310
  }
355
311
  export {
356
312
  createFabricServer,
313
+ startHttpServer,
357
314
  startStdioServer
358
315
  };
@@ -0,0 +1 @@
1
+ :root{--color-surface-canvas: #0b1016;--color-surface-panel: #0f172a;--color-surface-raised: #1e293b;--color-surface-overlay: #334155;--color-surface-code: #020617;--color-border-subtle: #1e293b;--color-border-default: #334155;--color-border-strong: #475569;--color-text-primary: #f8fafc;--color-text-secondary: #cbd5e1;--color-text-muted: #94a3b8;--color-text-dim: #64748b;--color-text-mono: #e2e8f0;--color-state-locked-bg: rgba(245, 158, 11, .08);--color-state-locked-border: #b45309;--color-state-locked-text: #fbbf24;--color-state-locked-accent: #f59e0b;--color-state-stale-bg: rgba(220, 38, 38, .1);--color-state-stale-border: #991b1b;--color-state-stale-text: #f87171;--color-state-stale-accent: #ef4444;--color-state-drift-bg: rgba(234, 88, 12, .1);--color-state-drift-border: #c2410c;--color-state-drift-text: #fb923c;--color-state-drift-accent: #ea580c;--color-state-approved-bg: rgba(34, 197, 94, .1);--color-state-approved-border: #166534;--color-state-approved-text: #4ade80;--color-state-approved-accent: #22c55e;--color-state-pending-bg: rgba(100, 116, 139, .1);--color-state-pending-border: #475569;--color-state-pending-text: #94a3b8;--color-state-pending-accent: #64748b;--color-source-ai-bg: rgba(99, 102, 241, .12);--color-source-ai-border: #4338ca;--color-source-ai-text: #a5b4fc;--color-source-ai-accent: #6366f1;--color-source-human-bg: rgba(20, 184, 166, .12);--color-source-human-border: #0f766e;--color-source-human-text: #5eead4;--color-source-human-accent: #14b8a6;--color-action-primary: #22c55e;--color-action-primary-hover: #16a34a;--color-action-primary-text: #052e16;--color-action-neutral: #334155;--color-action-neutral-hover: #475569;--color-action-danger: #dc2626;--font-family-mono: "Space Mono", "JetBrains Mono", "SF Mono", "Monaco", "Consolas", ui-monospace, monospace;--font-family-sans: "Inter", -apple-system, "Segoe UI", system-ui, sans-serif;--font-size-xs: 11px;--font-size-sm: 12px;--font-size-base: 13px;--font-size-md: 14px;--font-size-lg: 16px;--font-size-xl: 20px;--font-size-2xl: 24px;--font-weight-regular: 400;--font-weight-medium: 500;--font-weight-bold: 700;--line-height-tight: 1.25;--line-height-base: 1.5;--line-height-loose: 1.65;--letter-spacing-mono: 0;--letter-spacing-sans: -.01em;--letter-spacing-chip: .04em;--space-0: 0;--space-0-5: 2px;--space-1: 4px;--space-2: 8px;--space-3: 12px;--space-4: 16px;--space-5: 20px;--space-6: 24px;--space-8: 32px;--space-10: 40px;--space-12: 48px;--space-16: 64px;--radius-none: 0;--radius-sm: 3px;--radius-md: 6px;--radius-lg: 8px;--radius-pill: 999px;--shadow-card: 0 1px 2px rgba(0, 0, 0, .3), 0 1px 1px rgba(0, 0, 0, .15);--shadow-raised: 0 4px 12px rgba(0, 0, 0, .4);--shadow-focus-ring: 0 0 0 2px #0f172a, 0 0 0 4px #a5b4fc;--motion-duration-fast: .12s;--motion-duration-base: .18s;--motion-duration-slow: .28s;--motion-easing-standard: cubic-bezier(.4, 0, .2, 1);--motion-easing-decel: cubic-bezier(0, 0, .2, 1);--z-base: 0;--z-sticky: 10;--z-dropdown: 20;--z-popover: 30;--z-modal: 50;--z-toast: 100;--layout-sidebar-width: 240px;--layout-sidebar-width-collapsed: 64px;--layout-header-height: 48px;--layout-container-max-width: 1440px;--layout-container-padding: 24px}*{box-sizing:border-box}html,body{height:100%;margin:0}body{background:radial-gradient(circle at top left,rgba(99,102,241,.12),transparent 32rem),linear-gradient(135deg,rgba(15,23,42,.9),var(--color-surface-canvas) 52%);color:var(--color-text-secondary);font-family:var(--font-family-sans);font-size:var(--font-size-base);line-height:var(--line-height-base);-webkit-font-smoothing:antialiased}button,input{font:inherit}button:focus-visible,a:focus-visible,input:focus-visible,[tabindex]:focus-visible{box-shadow:var(--shadow-focus-ring);outline:0}.app-shell{display:grid;grid-template-columns:var(--layout-sidebar-width) minmax(0,1fr);min-height:100vh}.sidebar{background:color-mix(in srgb,var(--color-surface-panel) 92%,black);border-right:1px solid var(--color-border-subtle);padding:var(--space-4)}.brand{align-items:center;color:var(--color-text-primary);display:flex;font-family:var(--font-family-mono);font-size:var(--font-size-md);font-weight:var(--font-weight-bold);gap:var(--space-2);margin-bottom:var(--space-4);padding:var(--space-2)}.brand-logo{align-items:center;border:1px solid var(--color-text-mono);border-radius:var(--radius-sm);color:var(--color-text-mono);display:inline-flex;font-size:10px;height:18px;justify-content:center;width:18px}.brand-version{color:var(--color-text-dim);font-size:10px;font-weight:var(--font-weight-regular);margin-left:auto}.nav-item{align-items:center;border-radius:var(--radius-md);color:var(--color-text-muted);display:grid;font-size:var(--font-size-sm);gap:var(--space-2);grid-template-columns:8px 1fr;margin-bottom:2px;min-height:44px;padding:var(--space-2) var(--space-3);text-decoration:none;transition:background var(--motion-duration-base),color var(--motion-duration-base)}.nav-item:hover,.nav-item.active{background:var(--color-surface-raised);color:var(--color-text-primary)}.nav-item small{color:var(--color-text-dim);font-family:var(--font-family-mono);font-size:10px;grid-column:2;margin-top:-6px}.nav-item .dot{background:var(--color-text-dim);border-radius:50%;height:6px;width:6px}.nav-item.active .dot{background:var(--color-action-primary)}.muted-nav{opacity:.7}.nav-section{color:var(--color-text-dim);font-size:10px;letter-spacing:.08em;padding:var(--space-4) var(--space-3) var(--space-2);text-transform:uppercase}.main{display:flex;flex-direction:column;min-width:0}.header{align-items:center;background:#0f172adb;border-bottom:1px solid var(--color-border-subtle);display:flex;height:var(--layout-header-height);justify-content:space-between;padding:0 var(--space-6)}.breadcrumb,.port-label{color:var(--color-text-muted);font-family:var(--font-family-mono);font-size:var(--font-size-sm)}.breadcrumb .sep{color:var(--color-text-dim);margin:0 var(--space-1)}.breadcrumb strong{color:var(--color-text-primary);font-weight:var(--font-weight-regular)}.header-actions{align-items:center;display:flex;gap:var(--space-3)}.badge-live{align-items:center;border:1px solid rgba(34,197,94,.3);border-radius:var(--radius-pill);color:var(--color-state-approved-text);display:inline-flex;font-family:var(--font-family-mono);font-size:10px;gap:var(--space-1);padding:2px var(--space-2)}.badge-live.disconnected{border-color:var(--color-state-pending-border);color:var(--color-state-pending-text)}.pulse{background:currentColor;border-radius:50%;height:6px;width:6px}.view{flex:1;overflow:auto;padding:var(--space-6)}.view-header{align-items:flex-end;display:flex;justify-content:space-between;margin-bottom:var(--space-5);position:sticky;top:0;z-index:var(--z-sticky)}.view-title{color:var(--color-text-primary);font-size:var(--font-size-xl);margin:0}.view-subtitle,.muted{color:var(--color-text-muted);font-size:var(--font-size-sm);margin:var(--space-1) 0 0}.view-split{align-items:start;display:grid;gap:var(--space-5);grid-template-columns:minmax(0,2fr) minmax(280px,1fr)}.tree-panel,.detail-panel,.empty-card,.filter-bar{background:var(--color-surface-panel);border:1px solid var(--color-border-subtle);border-radius:var(--radius-lg);box-shadow:var(--shadow-card)}.tree-filter{border-bottom:1px solid var(--color-border-subtle);display:flex;gap:var(--space-3);padding:var(--space-4)}.tree-filter input,.annotate-input{background:var(--color-surface-code);border:1px solid var(--color-border-default);border-radius:var(--radius-md);color:var(--color-text-primary);font-family:var(--font-family-mono);min-height:44px;padding:var(--space-2) var(--space-3);width:100%}.status-line{color:var(--color-text-dim);display:flex;font-family:var(--font-family-mono);font-size:var(--font-size-xs);justify-content:space-between;padding:var(--space-3) var(--space-4)}.tree{font-family:var(--font-family-mono);padding:var(--space-2)}.tree-node{align-items:center;border-radius:var(--radius-md);color:var(--color-text-secondary);cursor:pointer;display:flex;gap:var(--space-2);min-height:40px;padding:var(--space-2) var(--space-3);position:relative;transition:background var(--motion-duration-fast),transform var(--motion-duration-fast)}.tree-node.is-readonly{cursor:default}.tree-node:hover:not(.is-readonly),.tree-node.is-selected{background:var(--color-surface-raised)}.tree-node.is-selected{box-shadow:inset 2px 0 0 var(--color-source-ai-accent)}.tree-node.is-locked{background:var(--color-state-locked-bg);box-shadow:inset 3px 0 0 var(--color-state-locked-border)}.tree-node.is-stale:before{background:var(--color-state-stale-text);border-radius:50%;content:"";height:6px;left:-2px;position:absolute;top:50%;transform:translateY(-50%);width:6px}.tree-caret{color:var(--color-text-dim);display:inline-flex;justify-content:center;transition:transform var(--motion-duration-base);width:14px}.tree-node.is-expanded .tree-caret{transform:rotate(90deg)}.tree-label{color:var(--color-text-primary);min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tree-level-1 .tree-label{color:var(--color-text-mono)}.tree-level-2 .tree-label{color:var(--color-text-secondary);font-weight:var(--font-weight-regular)}.tree-meta{align-items:center;display:flex;gap:var(--space-2);margin-left:auto}.tree-hash{color:var(--color-text-dim);font-size:var(--font-size-xs)}.tree-children{border-left:1px dashed var(--color-border-subtle);margin-left:18px;padding-left:var(--space-2)}.badge,.drift-pill,.source-badge,.filter-chip{align-items:center;border:1px solid transparent;border-radius:var(--radius-pill);display:inline-flex;font-family:var(--font-family-mono);font-size:10px;gap:var(--space-1);letter-spacing:var(--letter-spacing-chip);line-height:1;padding:3px 7px;text-transform:uppercase}.badge-level{background:var(--color-surface-overlay);color:var(--color-text-muted)}.badge-locked{background:var(--color-state-locked-bg);border-color:var(--color-state-locked-border);color:var(--color-state-locked-text)}.drift-dot{border-radius:50%;display:inline-block;height:6px;width:6px}.drift-pill,.drift-banner{background:var(--color-state-pending-bg);border-color:var(--color-state-pending-border);color:var(--color-state-pending-text)}.drift-drift{background:var(--color-state-drift-bg);border-color:var(--color-state-drift-border);color:var(--color-state-drift-text)}.drift-stale{background:var(--color-state-stale-bg);border-color:var(--color-state-stale-border);color:var(--color-state-stale-text)}.drift-locked{background:var(--color-state-locked-bg);border-color:var(--color-state-locked-border);color:var(--color-state-locked-text)}.drift-ok{background:var(--color-state-approved-bg);border-color:var(--color-state-approved-border);color:var(--color-state-approved-text)}.drift-banner{border-radius:var(--radius-md);display:flex;gap:var(--space-2);margin-bottom:var(--space-4);padding:var(--space-3)}.detail-panel{padding:var(--space-5);position:sticky;top:var(--space-6)}.detail-panel h3{color:var(--color-text-muted);font-size:var(--font-size-sm);letter-spacing:.06em;margin:0 0 var(--space-3);text-transform:uppercase}.kv{font-family:var(--font-family-mono);font-size:var(--font-size-sm)}.kv-row{border-bottom:1px dashed var(--color-border-subtle);display:flex;gap:var(--space-3);justify-content:space-between;padding:var(--space-1) 0}.kv-key{color:var(--color-text-muted)}.kv-value{color:var(--color-text-primary);overflow-wrap:anywhere;text-align:right}.code,.preview-body{background:var(--color-surface-code);color:var(--color-text-mono);font-family:var(--font-family-mono)}.code{border:1px solid var(--color-border-subtle);border-radius:var(--radius-md);margin-top:var(--space-3);overflow:auto;padding:var(--space-3)}.filter-bar{align-items:center;display:flex;gap:var(--space-2);margin-bottom:var(--space-4);padding:var(--space-3)}.filter-chip,.ghost-button{background:var(--color-surface-raised);border-color:var(--color-border-subtle);color:var(--color-text-muted);cursor:pointer;min-height:40px}.filter-chip.active,.filter-chip:hover,.ghost-button:hover{border-color:var(--color-border-default);color:var(--color-text-primary)}.filter-chip .count{background:var(--color-surface-code);border-radius:var(--radius-pill);padding:1px 5px}.filter-date{color:var(--color-text-muted);font-family:var(--font-family-mono);font-size:var(--font-size-sm);margin-left:auto}.filter-label{color:var(--color-text-dim);font-size:var(--font-size-xs);letter-spacing:.08em;text-transform:uppercase}.history-toolbar{margin-bottom:var(--space-5)}.history-slider{flex:1}.history-layout{grid-template-columns:minmax(340px,1.05fr) minmax(0,1.4fr)}.history-timeline-panel{min-width:0}.history-timeline-list{display:grid;gap:var(--space-3);max-height:calc(100vh - 280px);overflow:auto;padding:var(--space-4)}.history-timeline-item{background:transparent;border:0;cursor:pointer;padding:0;text-align:left}.history-timeline-item .timeline-entry{margin-bottom:0}.history-timeline-item.selected .timeline-entry{box-shadow:inset 0 0 0 1px var(--color-source-ai-accent)}.history-state-head{align-items:start}.history-state-title{color:var(--color-text-primary);font-family:var(--font-family-mono)}.ghost-button{border:1px solid var(--color-border-subtle);border-radius:var(--radius-md);padding:var(--space-2) var(--space-3)}.empty-card{color:var(--color-text-muted);padding:var(--space-6);text-align:center}.lock-grid{display:grid;gap:var(--space-4);grid-template-columns:repeat(auto-fill,minmax(360px,1fr))}.lock-card,.timeline-entry{background:var(--color-surface-panel);border:1px solid var(--color-border-subtle);border-radius:var(--radius-lg);box-shadow:var(--shadow-card);overflow:hidden}.lock-card{display:flex;flex-direction:column}.lock-drift{border-left:3px solid var(--color-state-drift-border)}.lock-ok{border-left:3px solid var(--color-state-approved-border)}.lock-head,.lock-foot{align-items:center;display:flex;gap:var(--space-3);padding:var(--space-4)}.lock-head{border-bottom:1px solid var(--color-border-subtle)}.lock-foot{border-top:1px solid var(--color-border-subtle);justify-content:space-between}.lock-icon{align-items:center;border-radius:var(--radius-md);display:inline-flex;font-family:var(--font-family-mono);height:28px;justify-content:center;width:28px}.lock-drift .lock-icon{background:var(--color-state-drift-bg);color:var(--color-state-drift-text)}.lock-ok .lock-icon{background:var(--color-state-approved-bg);color:var(--color-state-approved-text)}.lock-title{display:flex;flex:1;flex-direction:column;min-width:0}.lock-title strong{color:var(--color-text-primary);font-family:var(--font-family-mono);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.lock-title span,.meta-line{color:var(--color-text-dim);font-family:var(--font-family-mono);font-size:var(--font-size-xs)}.lock-body{display:flex;flex:1;flex-direction:column;gap:var(--space-3);padding:var(--space-4)}.hash-block{font-family:var(--font-family-mono);font-size:var(--font-size-sm)}.hash-row{display:grid;gap:var(--space-2);grid-template-columns:110px minmax(0,1fr);padding:var(--space-1) 0}.hash-key{color:var(--color-text-muted);font-size:10px;letter-spacing:.06em;text-transform:uppercase}.hash-value{color:var(--color-text-primary);overflow-wrap:anywhere}.hash-value.is-stale,.hash-value.is-accent{color:var(--color-state-drift-text)}.hash-value.is-stale{text-decoration:line-through}.preview{border:1px solid var(--color-border-subtle);border-radius:var(--radius-md);overflow:hidden}.preview-head{align-items:center;background:var(--color-surface-panel);border-bottom:1px solid var(--color-border-subtle);color:var(--color-text-dim);display:flex;font-family:var(--font-family-mono);font-size:10px;justify-content:space-between;letter-spacing:.06em;padding:var(--space-1) var(--space-3);text-transform:uppercase}.preview-body{display:block;line-height:1.55;margin:0;min-height:92px;overflow:auto;padding:var(--space-3);white-space:pre-wrap}.line-add,.line-del,.line-ctx{display:block}.line-add{background:#22c55e14;color:#86efac}.line-del{background:#dc262614;color:#fca5a5;text-decoration:line-through}.line-num{color:var(--color-text-dim);display:inline-block;padding-right:var(--space-2);text-align:right;-webkit-user-select:none;user-select:none;width:28px}.action-button{align-items:center;border:1px solid var(--color-border-default);border-radius:var(--radius-md);cursor:pointer;display:inline-flex;font-weight:var(--font-weight-medium);gap:var(--space-2);min-height:40px;padding:var(--space-2) var(--space-4);transition:transform var(--motion-duration-fast),background var(--motion-duration-base)}.action-button:active{transform:scale(.98)}.action-approve{background:var(--color-action-primary);border-color:transparent;color:var(--color-action-primary-text)}.action-annotate{background:var(--color-source-human-bg);border-color:var(--color-source-human-border);color:var(--color-source-human-text)}.action-success{background:var(--color-state-approved-bg);border-color:var(--color-state-approved-border);color:var(--color-state-approved-text);cursor:default}.action-busy{pointer-events:none}.action-error{border-color:var(--color-action-danger)}.spinner{animation:spin .7s linear infinite;border:2px solid currentColor;border-right-color:transparent;border-radius:50%;height:14px;width:14px}.source-badge{background:var(--color-surface-raised);color:var(--color-text-muted);min-height:28px}button.source-badge{cursor:pointer}.source-badge-ai{background:var(--color-source-ai-bg);border-color:var(--color-source-ai-border);color:var(--color-source-ai-text)}.source-badge-human{background:var(--color-source-human-bg);border-color:var(--color-source-human-border);color:var(--color-source-human-text)}.source-badge-outline{background:transparent}.source-badge.is-selected{box-shadow:inset 0 0 0 1px currentColor}.source-badge-dot{background:currentColor;border-radius:50%;height:6px;width:6px}.col-headers{display:grid;gap:var(--space-4);grid-template-columns:1fr 1fr;margin-bottom:var(--space-3)}.col-head{align-items:center;border-radius:var(--radius-md);display:flex;font-family:var(--font-family-mono);justify-content:space-between;padding:var(--space-3)}.col-head.ai{background:var(--color-source-ai-bg);color:var(--color-source-ai-text)}.col-head.human{background:var(--color-source-human-bg);color:var(--color-source-human-text)}.timeline-grid{display:grid;gap:0;grid-template-columns:1fr 40px 1fr;position:relative}.axis{align-items:center;display:flex;grid-column:2;grid-row:1 / span 999;justify-content:center;pointer-events:none}.axis-line{align-self:stretch;background:linear-gradient(var(--color-border-subtle),var(--color-border-default),var(--color-border-subtle));width:2px}.timeline-entry{margin-bottom:var(--space-3);padding:var(--space-3) var(--space-4);position:relative}.timeline-ai{border-left:3px solid var(--color-source-ai-border);grid-column:1}.timeline-human{border-right:3px solid var(--color-source-human-border);grid-column:3}.dot-axis{border:2px solid var(--color-surface-canvas);border-radius:50%;height:10px;position:absolute;top:16px;width:10px}.dot-axis.ai{background:var(--color-source-ai-accent);right:-27px}.dot-axis.human{background:var(--color-source-human-accent);left:-27px}.timeline-head,.entry-meta,.entry-foot{align-items:center;display:flex;flex-wrap:wrap;gap:var(--space-2)}.entry-time{color:var(--color-text-muted);font-family:var(--font-family-mono);font-size:var(--font-size-xs);margin-left:auto}.entry-title{color:var(--color-text-primary);font-size:var(--font-size-md);font-weight:var(--font-weight-medium);margin:var(--space-2) 0 0}.entry-meta{color:var(--color-text-muted);font-family:var(--font-family-mono);font-size:var(--font-size-xs);margin-top:var(--space-2)}.meta-key{color:var(--color-text-dim)}.entry-body{color:var(--color-text-secondary);line-height:var(--line-height-loose);margin-top:var(--space-3)}.diff-badge,.commit-hash{color:var(--color-text-muted);font-family:var(--font-family-mono);font-size:var(--font-size-xs)}.diff-badge{background:var(--color-surface-code);border:1px solid var(--color-border-subtle);border-radius:var(--radius-pill);padding:2px 6px}.annotate-form{display:grid;gap:var(--space-2);margin-top:var(--space-3)}.annotate-form label{color:var(--color-text-muted);font-family:var(--font-family-mono);font-size:var(--font-size-xs);text-transform:uppercase}.timeline-empty{grid-column:1 / -1}.doctor-toolbar{margin-bottom:var(--space-5)}.doctor-layout{display:grid;gap:var(--space-5)}.doctor-summary-grid,.doctor-panels{display:grid;gap:var(--space-4);grid-template-columns:repeat(auto-fit,minmax(220px,1fr))}.doctor-card,.doctor-summary-card{background:var(--color-surface-panel);border:1px solid var(--color-border-subtle);border-radius:var(--radius-lg);box-shadow:var(--shadow-card)}.doctor-summary-card{display:flex;flex-direction:column;gap:var(--space-2);padding:var(--space-4)}.doctor-summary-label,.doctor-summary-detail,.doctor-card-head span{color:var(--color-text-muted);font-family:var(--font-family-mono);font-size:var(--font-size-xs)}.doctor-summary-label{letter-spacing:.08em;text-transform:uppercase}.doctor-summary-value{color:var(--color-text-primary);font-size:var(--font-size-lg)}.doctor-card{overflow:hidden}.doctor-card-head{align-items:center;border-bottom:1px solid var(--color-border-subtle);display:flex;justify-content:space-between;padding:var(--space-4)}.doctor-card-head h3{color:var(--color-text-primary);font-size:var(--font-size-md);margin:0}.doctor-entry-list,.doctor-check-list{display:grid}.doctor-entry,.doctor-check{display:grid;gap:var(--space-2);padding:var(--space-4)}.doctor-entry+.doctor-entry,.doctor-check+.doctor-check{border-top:1px solid var(--color-border-subtle)}.doctor-entry strong,.doctor-check strong{color:var(--color-text-primary);font-family:var(--font-family-mono);font-size:var(--font-size-sm)}.doctor-entry span,.doctor-check p{color:var(--color-text-muted);margin:0}.doctor-check-head{align-items:center;display:flex;gap:var(--space-3);justify-content:space-between}.doctor-check-warn{background:linear-gradient(180deg,var(--color-state-locked-bg),transparent 90%)}.doctor-check-error{background:linear-gradient(180deg,var(--color-state-stale-bg),transparent 90%)}.doctor-check-ok{background:linear-gradient(180deg,var(--color-state-approved-bg),transparent 90%)}.doctor-empty{border:0;border-radius:0;box-shadow:none;margin:0}.live-region{clip:rect(0 0 0 0);height:1px;overflow:hidden;position:absolute;white-space:nowrap;width:1px}@keyframes spin{to{transform:rotate(360deg)}}@keyframes pulse{50%{opacity:.45}}@media(prefers-reduced-motion:no-preference){.pulse,.drift-dot.drift-drift,.drift-dot.drift-stale{animation:pulse 2.4s infinite}}@media(prefers-reduced-motion:reduce){*,*:before,*:after{animation-duration:0ms!important;scroll-behavior:auto!important;transition-duration:0ms!important}}@media(max-width:960px){.app-shell{grid-template-columns:var(--layout-sidebar-width-collapsed) minmax(0,1fr)}.brand span:not(.brand-logo),.nav-item span:not(.dot),.nav-item small,.nav-section,.muted-nav{display:none}.sidebar{padding:var(--space-3) var(--space-2)}.nav-item{grid-template-columns:1fr;justify-items:center}}@media(max-width:720px){.app-shell,.view-split,.timeline-grid,.col-headers,.doctor-panels,.doctor-summary-grid{grid-template-columns:1fr}.sidebar{border-bottom:1px solid var(--color-border-subtle);border-right:0;display:flex;overflow-x:auto}.main{min-height:0}.header{align-items:flex-start;flex-direction:column;height:auto;padding:var(--space-3)}.view{padding:var(--space-4)}.detail-panel{position:static}.axis,.dot-axis{display:none}.timeline-ai,.timeline-human{grid-column:1}}