proteum 2.2.8 → 2.3.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/AGENTS.md +5 -3
- package/README.md +50 -12
- package/agents/project/AGENTS.md +47 -10
- package/agents/project/CODING_STYLE.md +5 -1
- package/agents/project/client/AGENTS.md +2 -0
- package/agents/project/diagnostics.md +8 -5
- package/agents/project/optimizations.md +1 -0
- package/agents/project/root/AGENTS.md +18 -10
- package/agents/project/tests/AGENTS.md +6 -1
- package/agents/project/tests/e2e/AGENTS.md +13 -0
- package/agents/project/tests/e2e/REAL_WORLD_JOURNEY_TESTS.md +192 -0
- package/cli/commands/check.ts +21 -3
- package/cli/commands/configure.ts +1 -0
- package/cli/commands/connect.ts +40 -4
- package/cli/commands/diagnose.ts +136 -5
- package/cli/commands/doctor.ts +24 -4
- package/cli/commands/explain.ts +105 -6
- package/cli/commands/mcp.ts +16 -0
- package/cli/commands/orient.ts +66 -3
- package/cli/commands/perf.ts +118 -13
- package/cli/commands/runtime.ts +151 -0
- package/cli/commands/trace.ts +116 -21
- package/cli/mcp/provider.ts +365 -0
- package/cli/mcp/stdio.ts +16 -0
- package/cli/presentation/commands.ts +79 -22
- package/cli/presentation/devSession.ts +2 -0
- package/cli/runtime/commands.ts +95 -12
- package/cli/utils/agentOutput.ts +46 -0
- package/cli/utils/agents.ts +225 -48
- package/common/dev/inspection.ts +30 -9
- package/common/dev/mcpPayloads.ts +736 -0
- package/common/dev/mcpServer.ts +254 -0
- package/docs/agent-routing.md +126 -0
- package/docs/dev-commands.md +2 -0
- package/docs/dev-sessions.md +2 -1
- package/docs/diagnostics.md +68 -23
- package/docs/mcp.md +149 -0
- package/docs/migrate-from-2.1.3.md +15 -5
- package/docs/request-tracing.md +12 -6
- package/eslint.js +220 -0
- package/package.json +2 -1
- package/server/app/devMcp.ts +159 -0
- package/server/services/router/http/cache.ts +116 -0
- package/server/services/router/http/index.ts +94 -35
- package/server/services/router/index.ts +8 -11
- package/tests/agents-utils.test.cjs +89 -11
- package/tests/dev-transpile-watch.test.cjs +117 -8
- package/tests/eslint-rules.test.cjs +110 -0
- package/tests/inspection.test.cjs +67 -0
- package/tests/mcp.test.cjs +127 -0
- package/tests/router-cache-config.test.cjs +74 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
|
|
3
|
+
export type THttpCacheHeadersConfig = {
|
|
4
|
+
cacheControl?: string;
|
|
5
|
+
surrogateControl?: string | false;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export type TPublicAssetsCacheConfig = {
|
|
9
|
+
dev?: string;
|
|
10
|
+
versioned?: string;
|
|
11
|
+
unversioned?: string;
|
|
12
|
+
etag?: boolean;
|
|
13
|
+
lastModified?: boolean;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type THttpCacheConfig = {
|
|
17
|
+
html?: {
|
|
18
|
+
dynamic?: THttpCacheHeadersConfig;
|
|
19
|
+
static?: THttpCacheHeadersConfig;
|
|
20
|
+
};
|
|
21
|
+
publicAssets?: TPublicAssetsCacheConfig;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type TResolvedHttpCacheHeadersConfig = {
|
|
25
|
+
cacheControl: string;
|
|
26
|
+
surrogateControl: string | false;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type TResolvedPublicAssetsCacheConfig = {
|
|
30
|
+
dev: string;
|
|
31
|
+
versioned: string;
|
|
32
|
+
unversioned: string;
|
|
33
|
+
etag?: boolean;
|
|
34
|
+
lastModified?: boolean;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type TResolvedHttpCacheConfig = {
|
|
38
|
+
html: {
|
|
39
|
+
dynamic: TResolvedHttpCacheHeadersConfig;
|
|
40
|
+
static: TResolvedHttpCacheHeadersConfig;
|
|
41
|
+
};
|
|
42
|
+
publicAssets: TResolvedPublicAssetsCacheConfig;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const defaultHttpCacheConfig = {
|
|
46
|
+
html: {
|
|
47
|
+
dynamic: {
|
|
48
|
+
cacheControl: 'no-store, no-cache, must-revalidate, proxy-revalidate',
|
|
49
|
+
surrogateControl: 'no-store',
|
|
50
|
+
},
|
|
51
|
+
static: {
|
|
52
|
+
cacheControl: 'public, max-age=0, must-revalidate',
|
|
53
|
+
surrogateControl: false,
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
publicAssets: {
|
|
57
|
+
dev: 'no-store',
|
|
58
|
+
versioned: 'public, max-age=31536000, immutable',
|
|
59
|
+
unversioned: 'public, max-age=0, must-revalidate',
|
|
60
|
+
},
|
|
61
|
+
} satisfies TResolvedHttpCacheConfig;
|
|
62
|
+
|
|
63
|
+
type TPublicAssetRequest = {
|
|
64
|
+
originalUrl?: string;
|
|
65
|
+
url?: string;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
type TPublicAssetResponse = {
|
|
69
|
+
req?: TPublicAssetRequest;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const hashedPublicAssetPattern = /(^|[-_.])[a-f0-9]{6,}(?=(\.[^.]+)+$)/i;
|
|
73
|
+
|
|
74
|
+
export const resolveHttpCacheConfig = (config?: THttpCacheConfig): TResolvedHttpCacheConfig => ({
|
|
75
|
+
html: {
|
|
76
|
+
dynamic: {
|
|
77
|
+
...defaultHttpCacheConfig.html.dynamic,
|
|
78
|
+
...(config?.html?.dynamic || {}),
|
|
79
|
+
},
|
|
80
|
+
static: {
|
|
81
|
+
...defaultHttpCacheConfig.html.static,
|
|
82
|
+
...(config?.html?.static || {}),
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
publicAssets: {
|
|
86
|
+
...defaultHttpCacheConfig.publicAssets,
|
|
87
|
+
...(config?.publicAssets || {}),
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
export const isVersionedPublicAssetRequest = (
|
|
92
|
+
res: undefined | TPublicAssetResponse,
|
|
93
|
+
filePath: string,
|
|
94
|
+
) => {
|
|
95
|
+
const requestUrl = res?.req?.originalUrl || res?.req?.url || '';
|
|
96
|
+
const searchParams = new URL(requestUrl, 'http://proteum.local').searchParams;
|
|
97
|
+
if (searchParams.has('v')) return true;
|
|
98
|
+
|
|
99
|
+
return hashedPublicAssetPattern.test(path.basename(filePath));
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export const resolvePublicAssetCacheControl = ({
|
|
103
|
+
res,
|
|
104
|
+
filePath,
|
|
105
|
+
profile,
|
|
106
|
+
cache,
|
|
107
|
+
}: {
|
|
108
|
+
res: undefined | TPublicAssetResponse;
|
|
109
|
+
filePath: string;
|
|
110
|
+
profile: string;
|
|
111
|
+
cache: TResolvedPublicAssetsCacheConfig;
|
|
112
|
+
}) => {
|
|
113
|
+
if (profile === 'dev') return cache.dev;
|
|
114
|
+
|
|
115
|
+
return isVersionedPublicAssetRequest(res, filePath) ? cache.versioned : cache.unversioned;
|
|
116
|
+
};
|
|
@@ -8,7 +8,10 @@ import express from 'express';
|
|
|
8
8
|
import http from 'http';
|
|
9
9
|
import https from 'https';
|
|
10
10
|
import path from 'path';
|
|
11
|
+
import { randomUUID } from 'crypto';
|
|
11
12
|
import cors, { CorsOptions } from 'cors';
|
|
13
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
14
|
+
import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
|
|
12
15
|
|
|
13
16
|
// Middlewares (npm)
|
|
14
17
|
import morgan from 'morgan';
|
|
@@ -37,9 +40,16 @@ import {
|
|
|
37
40
|
parseConnectedProjectProxyPath,
|
|
38
41
|
} from '@common/connectedProjects';
|
|
39
42
|
import { profilerTraceRequestIdHeader } from '@common/dev/profiler';
|
|
43
|
+
import { createProteumMcpServer } from '@common/dev/mcpServer';
|
|
44
|
+
import { createRuntimeProteumMcpProvider } from '@server/app/devMcp';
|
|
40
45
|
|
|
41
46
|
// Middlewaees (core)
|
|
42
47
|
import { isMutipart, MiddlewareFormData } from './multipart';
|
|
48
|
+
import {
|
|
49
|
+
resolveHttpCacheConfig,
|
|
50
|
+
resolvePublicAssetCacheControl,
|
|
51
|
+
type THttpCacheConfig,
|
|
52
|
+
} from './cache';
|
|
43
53
|
|
|
44
54
|
/*----------------------------------
|
|
45
55
|
- CONFIG
|
|
@@ -58,12 +68,15 @@ export type Config = {
|
|
|
58
68
|
maxSize: string; // Expression package bytes
|
|
59
69
|
};
|
|
60
70
|
csp: { default?: string[]; styles?: string[]; images?: string[]; scripts: string[] };
|
|
71
|
+
cache?: THttpCacheConfig;
|
|
61
72
|
cors?: CorsOptions;
|
|
62
73
|
helmet?: Parameters<typeof helmet>[0];
|
|
63
74
|
};
|
|
64
75
|
|
|
65
76
|
export type Hooks = {};
|
|
66
77
|
|
|
78
|
+
export { resolveHttpCacheConfig, type THttpCacheConfig } from './cache';
|
|
79
|
+
|
|
67
80
|
type TContentSecurityPolicyOptions = NonNullable<Parameters<typeof helmet.contentSecurityPolicy>[0]>;
|
|
68
81
|
type TContentSecurityPolicyDirectives = NonNullable<TContentSecurityPolicyOptions['directives']>;
|
|
69
82
|
type TDevSessionAuthService = {
|
|
@@ -94,37 +107,9 @@ const createContentSecurityPolicy = (config: Config['csp']): TContentSecurityPol
|
|
|
94
107
|
};
|
|
95
108
|
};
|
|
96
109
|
|
|
97
|
-
const immutablePublicAssetCacheControl = 'public, max-age=31536000, immutable';
|
|
98
|
-
const devPublicAssetCacheControl = 'no-store';
|
|
99
|
-
const revalidatedPublicAssetCacheControl = 'public, max-age=0, must-revalidate';
|
|
100
|
-
const hashedPublicAssetPattern = /(^|[-_.])[a-f0-9]{6,}(?=(\.[^.]+)+$)/i;
|
|
101
110
|
const connectedProjectBootRetryCount = 10;
|
|
102
111
|
const connectedProjectBootRetryDelayMs = 5_000;
|
|
103
112
|
|
|
104
|
-
const isVersionedPublicAssetRequest = (res: undefined | express.Response | http.ServerResponse, filePath: string) => {
|
|
105
|
-
const request =
|
|
106
|
-
res && typeof res === 'object' && 'req' in res
|
|
107
|
-
? ((res as express.Response | (http.ServerResponse & { req?: express.Request })).req ?? undefined)
|
|
108
|
-
: undefined;
|
|
109
|
-
const requestUrl = request?.originalUrl || request?.url || '';
|
|
110
|
-
const searchParams = new URL(requestUrl, 'http://proteum.local').searchParams;
|
|
111
|
-
if (searchParams.has('v')) return true;
|
|
112
|
-
|
|
113
|
-
return hashedPublicAssetPattern.test(path.basename(filePath));
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
const resolvePublicAssetCacheControl = ({
|
|
117
|
-
res,
|
|
118
|
-
filePath,
|
|
119
|
-
profile,
|
|
120
|
-
}: {
|
|
121
|
-
res: undefined | express.Response | http.ServerResponse;
|
|
122
|
-
filePath: string;
|
|
123
|
-
profile: string;
|
|
124
|
-
}) => {
|
|
125
|
-
if (profile === 'dev') return devPublicAssetCacheControl;
|
|
126
|
-
return isVersionedPublicAssetRequest(res, filePath) ? immutablePublicAssetCacheControl : revalidatedPublicAssetCacheControl;
|
|
127
|
-
};
|
|
128
113
|
const wait = async (durationMs: number) =>
|
|
129
114
|
await new Promise<void>((resolve) => {
|
|
130
115
|
setTimeout(resolve, durationMs);
|
|
@@ -406,20 +391,22 @@ export default class HttpServer<TRouter extends TServerRouter = TServerRouter> {
|
|
|
406
391
|
// Normalement, seulement utile pour le mode production,
|
|
407
392
|
// Quand mode debug, les ressources client semblent servies par le dev middlewae
|
|
408
393
|
// Sauf que les ressources serveur ne semblent pas trouvées par le dev-middleware
|
|
409
|
-
const
|
|
394
|
+
const publicAssetCache = resolveHttpCacheConfig(this.config.cache).publicAssets;
|
|
395
|
+
const defaultPublicAssetValidators = this.app.env.profile !== 'dev';
|
|
396
|
+
const publicAssetEtag = publicAssetCache.etag ?? defaultPublicAssetValidators;
|
|
397
|
+
const publicAssetLastModified = publicAssetCache.lastModified ?? defaultPublicAssetValidators;
|
|
398
|
+
|
|
410
399
|
routes.use(compression());
|
|
411
400
|
routes.use('/public', cors());
|
|
412
401
|
routes.use(
|
|
413
402
|
'/public',
|
|
414
403
|
express.static(path.join(Container.path.root, APP_OUTPUT_DIR, 'public'), {
|
|
415
404
|
dotfiles: 'deny',
|
|
416
|
-
etag:
|
|
417
|
-
lastModified:
|
|
405
|
+
etag: publicAssetEtag,
|
|
406
|
+
lastModified: publicAssetLastModified,
|
|
418
407
|
setHeaders: (res, filePath) => {
|
|
419
|
-
if (
|
|
420
|
-
|
|
421
|
-
res.removeHeader('Last-Modified');
|
|
422
|
-
}
|
|
408
|
+
if (!publicAssetEtag) res.removeHeader('ETag');
|
|
409
|
+
if (!publicAssetLastModified) res.removeHeader('Last-Modified');
|
|
423
410
|
|
|
424
411
|
res.setHeader(
|
|
425
412
|
'Cache-Control',
|
|
@@ -427,6 +414,7 @@ export default class HttpServer<TRouter extends TServerRouter = TServerRouter> {
|
|
|
427
414
|
res,
|
|
428
415
|
filePath,
|
|
429
416
|
profile: this.app.env.profile,
|
|
417
|
+
cache: publicAssetCache,
|
|
430
418
|
}),
|
|
431
419
|
);
|
|
432
420
|
},
|
|
@@ -510,6 +498,8 @@ export default class HttpServer<TRouter extends TServerRouter = TServerRouter> {
|
|
|
510
498
|
private registerDevTraceRoutes(routes: express.Express) {
|
|
511
499
|
if (!__DEV__ || this.app.env.profile !== 'dev') return;
|
|
512
500
|
|
|
501
|
+
this.registerDevMcpRoute(routes);
|
|
502
|
+
|
|
513
503
|
if (this.app.container.Trace.isDevTraceEnabled()) {
|
|
514
504
|
routes.get('/__proteum/trace/requests', (req, res) => {
|
|
515
505
|
const rawLimit = Array.isArray(req.query.limit) ? req.query.limit[0] : req.query.limit;
|
|
@@ -836,4 +826,73 @@ export default class HttpServer<TRouter extends TServerRouter = TServerRouter> {
|
|
|
836
826
|
private getCronManager() {
|
|
837
827
|
return (this.app as typeof this.app & { Cron?: CronManager }).Cron;
|
|
838
828
|
}
|
|
829
|
+
|
|
830
|
+
private registerDevMcpRoute(routes: express.Express) {
|
|
831
|
+
type TMcpTransportEntry = {
|
|
832
|
+
server: ReturnType<typeof createProteumMcpServer>;
|
|
833
|
+
transport: StreamableHTTPServerTransport;
|
|
834
|
+
};
|
|
835
|
+
const transports = new Map<string, TMcpTransportEntry>();
|
|
836
|
+
const readSessionId = (req: express.Request) => {
|
|
837
|
+
const value = req.headers['mcp-session-id'];
|
|
838
|
+
if (Array.isArray(value)) return value[0];
|
|
839
|
+
return typeof value === 'string' && value.trim() ? value : undefined;
|
|
840
|
+
};
|
|
841
|
+
const writeJsonRpcError = (res: express.Response, statusCode: number, message: string) => {
|
|
842
|
+
res.status(statusCode).json({
|
|
843
|
+
jsonrpc: '2.0',
|
|
844
|
+
error: {
|
|
845
|
+
code: -32000,
|
|
846
|
+
message,
|
|
847
|
+
},
|
|
848
|
+
id: null,
|
|
849
|
+
});
|
|
850
|
+
};
|
|
851
|
+
|
|
852
|
+
routes.all('/__proteum/mcp', async (req, res) => {
|
|
853
|
+
const sessionId = readSessionId(req);
|
|
854
|
+
let entry = sessionId ? transports.get(sessionId) : undefined;
|
|
855
|
+
|
|
856
|
+
try {
|
|
857
|
+
if (!entry && !sessionId && req.method === 'POST' && isInitializeRequest(req.body)) {
|
|
858
|
+
const provider = createRuntimeProteumMcpProvider({
|
|
859
|
+
app: this.app,
|
|
860
|
+
publicUrl: this.publicUrl,
|
|
861
|
+
routerPort: this.config.port,
|
|
862
|
+
});
|
|
863
|
+
const server = createProteumMcpServer({
|
|
864
|
+
provider,
|
|
865
|
+
version: String(process.env.npm_package_version || 'runtime'),
|
|
866
|
+
});
|
|
867
|
+
const transport = new StreamableHTTPServerTransport({
|
|
868
|
+
sessionIdGenerator: () => randomUUID(),
|
|
869
|
+
onsessioninitialized: (initializedSessionId) => {
|
|
870
|
+
transports.set(initializedSessionId, { server, transport });
|
|
871
|
+
},
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
transport.onclose = () => {
|
|
875
|
+
const transportSessionId = transport.sessionId;
|
|
876
|
+
if (transportSessionId) transports.delete(transportSessionId);
|
|
877
|
+
void server.close().catch(() => undefined);
|
|
878
|
+
};
|
|
879
|
+
|
|
880
|
+
await server.connect(transport);
|
|
881
|
+
entry = { server, transport };
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
if (!entry) {
|
|
885
|
+
writeJsonRpcError(res, 400, 'Bad Request: initialize the Proteum MCP session before sending tool or resource requests.');
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
await entry.transport.handleRequest(req, res, req.body);
|
|
890
|
+
} catch (error) {
|
|
891
|
+
console.error('Error handling Proteum MCP request:', error);
|
|
892
|
+
if (!res.headersSent) {
|
|
893
|
+
writeJsonRpcError(res, 500, 'Internal Proteum MCP server error.');
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
});
|
|
897
|
+
}
|
|
839
898
|
}
|
|
@@ -52,7 +52,7 @@ import { AnyRouterService } from './service';
|
|
|
52
52
|
import ServerRequest from './request';
|
|
53
53
|
import ServerResponse, { TRouterContext, TRouterContextServices } from './response';
|
|
54
54
|
import Page from './response/page';
|
|
55
|
-
import HTTP, { Config as HttpServiceConfig } from './http';
|
|
55
|
+
import HTTP, { Config as HttpServiceConfig, resolveHttpCacheConfig } from './http';
|
|
56
56
|
import DocumentRenderer from './response/page/document';
|
|
57
57
|
import { loadGeneratedRuntimeBundle } from './generatedRuntime';
|
|
58
58
|
|
|
@@ -97,9 +97,6 @@ export type TApiResponseData = { data: any; triggers?: { [cle: string]: any } };
|
|
|
97
97
|
|
|
98
98
|
export type HttpHeaders = { [cle: string]: string };
|
|
99
99
|
|
|
100
|
-
const dynamicHtmlCacheControl = 'no-store, no-cache, must-revalidate, proxy-revalidate';
|
|
101
|
-
const staticHtmlCacheControl = 'public, max-age=0, must-revalidate';
|
|
102
|
-
|
|
103
100
|
/*----------------------------------
|
|
104
101
|
- SERVICE CONFIG
|
|
105
102
|
----------------------------------*/
|
|
@@ -1136,15 +1133,15 @@ export default class ServerRouter<
|
|
|
1136
1133
|
}
|
|
1137
1134
|
|
|
1138
1135
|
private applyHtmlCacheHeaders(res: express.Response, isStaticHtml: boolean) {
|
|
1139
|
-
|
|
1136
|
+
const cache = resolveHttpCacheConfig(this.config.http.cache);
|
|
1137
|
+
const htmlCache = isStaticHtml ? cache.html.static : cache.html.dynamic;
|
|
1138
|
+
|
|
1139
|
+
if (htmlCache.surrogateControl === false) {
|
|
1140
1140
|
res.removeHeader('Surrogate-Control');
|
|
1141
|
-
|
|
1142
|
-
|
|
1141
|
+
} else {
|
|
1142
|
+
res.setHeader('Surrogate-Control', htmlCache.surrogateControl);
|
|
1143
1143
|
}
|
|
1144
1144
|
|
|
1145
|
-
|
|
1146
|
-
// https://github.com/helmetjs/nocache/blob/main/index.ts
|
|
1147
|
-
res.setHeader('Surrogate-Control', 'no-store');
|
|
1148
|
-
res.setHeader('Cache-Control', dynamicHtmlCacheControl);
|
|
1145
|
+
res.setHeader('Cache-Control', htmlCache.cacheControl);
|
|
1149
1146
|
}
|
|
1150
1147
|
}
|
|
@@ -24,7 +24,17 @@ const createCoreFixture = () => {
|
|
|
24
24
|
|
|
25
25
|
writeFile(path.join(agentsRoot, 'AGENTS.md'), '# Root Contract\n\n- Root rule\n');
|
|
26
26
|
writeFile(path.join(agentsRoot, 'CODING_STYLE.md'), '# Coding Style\n\n- Style rule\n');
|
|
27
|
+
writeFile(path.join(agentsRoot, 'diagnostics.md'), '# Diagnostics\n\n- Diagnostics rule\n');
|
|
28
|
+
writeFile(path.join(agentsRoot, 'optimizations.md'), '# Optimizations\n\n- Optimization rule\n');
|
|
27
29
|
writeFile(path.join(agentsRoot, 'client', 'AGENTS.md'), '# Client Rules\n\n- Client rule\n');
|
|
30
|
+
writeFile(path.join(agentsRoot, 'client', 'pages', 'AGENTS.md'), '# Page Rules\n\n- Page rule\n');
|
|
31
|
+
writeFile(path.join(agentsRoot, 'server', 'routes', 'AGENTS.md'), '# Route Rules\n\n- Route rule\n');
|
|
32
|
+
writeFile(path.join(agentsRoot, 'server', 'services', 'AGENTS.md'), '# Service Rules\n\n- Service rule\n');
|
|
33
|
+
writeFile(path.join(agentsRoot, 'tests', 'e2e', 'AGENTS.md'), '# E2E Rules\n\n- E2E rule\n');
|
|
34
|
+
writeFile(
|
|
35
|
+
path.join(agentsRoot, 'tests', 'e2e', 'REAL_WORLD_JOURNEY_TESTS.md'),
|
|
36
|
+
'# Real World Journey Tests\n\n- Journey rule\n',
|
|
37
|
+
);
|
|
28
38
|
|
|
29
39
|
return root;
|
|
30
40
|
};
|
|
@@ -52,7 +62,7 @@ const createAppFixture = () => {
|
|
|
52
62
|
return appRoot;
|
|
53
63
|
};
|
|
54
64
|
|
|
55
|
-
test('standalone configure creates tracked instruction files with
|
|
65
|
+
test('standalone configure creates tracked instruction files with routing contract and split docs', () => {
|
|
56
66
|
const coreRoot = createCoreFixture();
|
|
57
67
|
const appRoot = createAppFixture();
|
|
58
68
|
const result = configureProjectAgentInstructions({ appRoot, coreRoot });
|
|
@@ -63,10 +73,16 @@ test('standalone configure creates tracked instruction files with embedded corpu
|
|
|
63
73
|
assert.equal(result.blocked.length, 0);
|
|
64
74
|
assert.match(agentsContent, /^# Proteum Instructions/m);
|
|
65
75
|
assert.match(agentsContent, /<!-- proteum-instructions:start -->/);
|
|
66
|
-
assert.match(agentsContent, /##
|
|
67
|
-
assert.match(agentsContent,
|
|
68
|
-
assert.
|
|
69
|
-
assert.match(codingStyleContent, /## Source:
|
|
76
|
+
assert.match(agentsContent, /## Agent Routing Contract/);
|
|
77
|
+
assert.match(agentsContent, /npx proteum orient <query>/);
|
|
78
|
+
assert.doesNotMatch(agentsContent, /## Source: CODING_STYLE\.md/);
|
|
79
|
+
assert.match(codingStyleContent, /## Source: CODING_STYLE\.md/);
|
|
80
|
+
assert.match(codingStyleContent, /## Coding Style/);
|
|
81
|
+
assert.doesNotMatch(codingStyleContent, /## Source: client\/AGENTS\.md/);
|
|
82
|
+
assert.equal(fs.existsSync(path.join(appRoot, 'tests', 'e2e', 'AGENTS.md')), true);
|
|
83
|
+
assert.equal(fs.existsSync(path.join(appRoot, 'tests', 'e2e', 'REAL_WORLD_JOURNEY_TESTS.md')), true);
|
|
84
|
+
assert.match(fs.readFileSync(path.join(appRoot, 'tests', 'e2e', 'REAL_WORLD_JOURNEY_TESTS.md'), 'utf8'), /Journey rule/);
|
|
85
|
+
assert.doesNotMatch(fs.readFileSync(path.join(appRoot, 'tests', 'e2e', 'REAL_WORLD_JOURNEY_TESTS.md'), 'utf8'), /## Source: CODING_STYLE\.md/);
|
|
70
86
|
assert.doesNotMatch(agentsContent, /Before reading or applying instructions from this file/);
|
|
71
87
|
assert.doesNotMatch(gitignoreContent, /Proteum-managed instruction files/);
|
|
72
88
|
assert.doesNotMatch(gitignoreContent, /^\/AGENTS\.md$/m);
|
|
@@ -102,7 +118,8 @@ test('configure preserves project content outside the managed section', () => {
|
|
|
102
118
|
const content = fs.readFileSync(path.join(appRoot, 'AGENTS.md'), 'utf8');
|
|
103
119
|
assert.match(content, /# Product Notes/);
|
|
104
120
|
assert.match(content, /Keep this product note\./);
|
|
105
|
-
assert.match(content, /##
|
|
121
|
+
assert.match(content, /## Agent Routing Contract/);
|
|
122
|
+
assert.doesNotMatch(content, /## Source: CODING_STYLE\.md/);
|
|
106
123
|
assert.doesNotMatch(content, /Old managed content/);
|
|
107
124
|
assert.match(content, /# Local Footer/);
|
|
108
125
|
assert.match(content, /Keep this footer\./);
|
|
@@ -144,7 +161,8 @@ test('configure preserves project content around legacy managed stubs', () => {
|
|
|
144
161
|
assert.match(content, /## Product Bootstrap/);
|
|
145
162
|
assert.match(content, /Keep these local bootstrap notes\./);
|
|
146
163
|
assert.match(content, /# Proteum Instructions/);
|
|
147
|
-
assert.match(content, /##
|
|
164
|
+
assert.match(content, /## Agent Routing Contract/);
|
|
165
|
+
assert.doesNotMatch(content, /## Source: CODING_STYLE\.md/);
|
|
148
166
|
assert.doesNotMatch(content, /# Proteum Managed Instructions/);
|
|
149
167
|
assert.doesNotMatch(content, /Before reading or applying instructions from this file/);
|
|
150
168
|
assert.match(content, /## Local Footer/);
|
|
@@ -159,15 +177,75 @@ test('monorepo configure writes root and app instruction files', () => {
|
|
|
159
177
|
fs.mkdirSync(path.join(monorepoRoot, '.git'));
|
|
160
178
|
fs.mkdirSync(path.join(appRoot, 'client'), { recursive: true });
|
|
161
179
|
|
|
180
|
+
configureProjectAgentInstructions({ appRoot, coreRoot });
|
|
181
|
+
|
|
162
182
|
const result = configureProjectAgentInstructions({ appRoot, coreRoot, monorepoRoot });
|
|
163
183
|
|
|
164
184
|
assert.equal(result.mode, 'monorepo');
|
|
165
185
|
assert.equal(resolveProjectAgentMonorepoRoot(appRoot), fs.realpathSync(monorepoRoot));
|
|
166
|
-
assert.match(fs.readFileSync(path.join(monorepoRoot, 'AGENTS.md'), 'utf8'), /##
|
|
167
|
-
assert.match(fs.readFileSync(path.join(
|
|
186
|
+
assert.match(fs.readFileSync(path.join(monorepoRoot, 'AGENTS.md'), 'utf8'), /## Agent Routing Contract/);
|
|
187
|
+
assert.match(fs.readFileSync(path.join(monorepoRoot, 'CODING_STYLE.md'), 'utf8'), /## Source: CODING_STYLE\.md/);
|
|
188
|
+
assert.match(fs.readFileSync(path.join(monorepoRoot, 'diagnostics.md'), 'utf8'), /## Source: diagnostics\.md/);
|
|
189
|
+
assert.match(fs.readFileSync(path.join(monorepoRoot, 'optimizations.md'), 'utf8'), /## Source: optimizations\.md/);
|
|
190
|
+
assert.match(fs.readFileSync(path.join(monorepoRoot, 'tests', 'e2e', 'AGENTS.md'), 'utf8'), /## Source: tests\/e2e\/AGENTS\.md/);
|
|
191
|
+
assert.match(fs.readFileSync(path.join(monorepoRoot, 'tests', 'e2e', 'REAL_WORLD_JOURNEY_TESTS.md'), 'utf8'), /## Source: tests\/e2e\/REAL_WORLD_JOURNEY_TESTS\.md/);
|
|
192
|
+
assert.doesNotMatch(fs.readFileSync(path.join(monorepoRoot, 'tests', 'e2e', 'REAL_WORLD_JOURNEY_TESTS.md'), 'utf8'), /## Source: CODING_STYLE\.md/);
|
|
193
|
+
assert.equal(fs.existsSync(path.join(appRoot, 'tests', 'e2e', 'AGENTS.md')), false);
|
|
194
|
+
assert.match(fs.readFileSync(path.join(appRoot, 'AGENTS.md'), 'utf8'), /## Agent Routing Contract/);
|
|
195
|
+
assert.match(fs.readFileSync(path.join(appRoot, 'client', 'AGENTS.md'), 'utf8'), /## Source: client\/AGENTS\.md/);
|
|
196
|
+
assert.equal(fs.existsSync(path.join(appRoot, 'CODING_STYLE.md')), false);
|
|
197
|
+
assert.equal(fs.existsSync(path.join(appRoot, 'diagnostics.md')), false);
|
|
198
|
+
assert.equal(fs.existsSync(path.join(appRoot, 'optimizations.md')), false);
|
|
199
|
+
assert.equal(result.removed.some((entry) => entry.endsWith('/apps/product/CODING_STYLE.md')), true);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test('monorepo configure preserves local app-root documents', () => {
|
|
203
|
+
const coreRoot = createCoreFixture();
|
|
204
|
+
const monorepoRoot = makeTempRoot();
|
|
205
|
+
const appRoot = path.join(monorepoRoot, 'apps', 'product');
|
|
206
|
+
const localCodingStylePath = path.join(appRoot, 'CODING_STYLE.md');
|
|
207
|
+
|
|
208
|
+
fs.mkdirSync(path.join(monorepoRoot, '.git'));
|
|
209
|
+
writeFile(localCodingStylePath, '# Local Coding Style\n\n- Keep this app-local override.\n');
|
|
210
|
+
|
|
211
|
+
const result = configureProjectAgentInstructions({ appRoot, coreRoot, monorepoRoot });
|
|
212
|
+
|
|
213
|
+
assert.match(fs.readFileSync(path.join(monorepoRoot, 'CODING_STYLE.md'), 'utf8'), /## Source: CODING_STYLE\.md/);
|
|
214
|
+
assert.match(fs.readFileSync(localCodingStylePath, 'utf8'), /Keep this app-local override/);
|
|
215
|
+
assert.equal(result.removed.some((entry) => entry.endsWith('/apps/product/CODING_STYLE.md')), false);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test('monorepo configure strips retired managed sections from local app-root documents', () => {
|
|
219
|
+
const coreRoot = createCoreFixture();
|
|
220
|
+
const monorepoRoot = makeTempRoot();
|
|
221
|
+
const appRoot = path.join(monorepoRoot, 'apps', 'product');
|
|
222
|
+
const localCodingStylePath = path.join(appRoot, 'CODING_STYLE.md');
|
|
223
|
+
|
|
224
|
+
fs.mkdirSync(path.join(monorepoRoot, '.git'));
|
|
225
|
+
fs.mkdirSync(appRoot, { recursive: true });
|
|
226
|
+
configureProjectAgentInstructions({ appRoot, coreRoot });
|
|
227
|
+
|
|
228
|
+
const managedContent = fs.readFileSync(localCodingStylePath, 'utf8');
|
|
229
|
+
writeFile(
|
|
230
|
+
localCodingStylePath,
|
|
231
|
+
[
|
|
232
|
+
'# Local Coding Style',
|
|
233
|
+
'',
|
|
234
|
+
'- Keep this app-local override.',
|
|
235
|
+
'',
|
|
236
|
+
managedContent,
|
|
237
|
+
].join('\n'),
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
const result = configureProjectAgentInstructions({ appRoot, coreRoot, monorepoRoot });
|
|
241
|
+
const retainedContent = fs.readFileSync(localCodingStylePath, 'utf8');
|
|
242
|
+
|
|
243
|
+
assert.match(retainedContent, /Keep this app-local override/);
|
|
244
|
+
assert.doesNotMatch(retainedContent, /proteum-instructions:start/);
|
|
245
|
+
assert.equal(result.updated.some((entry) => entry.endsWith('/apps/product/CODING_STYLE.md')), true);
|
|
168
246
|
});
|
|
169
247
|
|
|
170
|
-
test('configure migrates legacy managed symlinks to
|
|
248
|
+
test('configure migrates legacy managed symlinks to tracked files', () => {
|
|
171
249
|
const coreRoot = createCoreFixture();
|
|
172
250
|
const appRoot = createAppFixture();
|
|
173
251
|
const installedCoreRoot = createCoreFixture();
|
|
@@ -203,5 +281,5 @@ test('configure reports blocked paths unless overwrite is allowed', () => {
|
|
|
203
281
|
|
|
204
282
|
assert.equal(result.overwritten.some((entry) => entry.endsWith('/CODING_STYLE.md')), true);
|
|
205
283
|
assert.equal(fs.lstatSync(blockedPath).isFile(), true);
|
|
206
|
-
assert.match(fs.readFileSync(blockedPath, 'utf8'), /## Source:
|
|
284
|
+
assert.match(fs.readFileSync(blockedPath, 'utf8'), /## Source: CODING_STYLE\.md/);
|
|
207
285
|
});
|