@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/chunk-KZO24GUQ.js +199 -0
- package/dist/http-3BNWQWPP.js +1608 -0
- package/dist/index.d.ts +10 -1
- package/dist/index.js +155 -198
- package/dist/static/assets/index-D45wW11O.css +1 -0
- package/dist/static/assets/index-D_EcxWYV.js +5 -0
- package/dist/static/index.html +14 -0
- package/package.json +13 -8
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 {
|
|
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/
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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:
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
|
96
|
-
|
|
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
|
|
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
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
157
|
-
|
|
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
|
|
178
|
-
|
|
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 {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
import {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
-
|
|
244
|
-
|
|
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
|
|
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
|
|
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] =
|
|
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
|
|
296
|
-
|
|
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: "
|
|
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}}
|