feat(phase0): complete GC1 feasibility and GC2 shared format contracts
- Record user evidence for directory fallback to complete GC1 (10/10 checks passed) - Expand Format Specification 0.1 to Revision 0.2 with normative shared contracts - Author JSON Schema Draft-07 at schema/xzbt-0.1.schema.json - Implement zero-dependency semantic validator at tools/validate-exhibit.mjs - Create 12-case conformance fixture suite and automated test runner (12/12 passing) - Update implementation status, verification gates, and gap closure decisions - Add devlog entry covering stall recovery and Phase 0 current state
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* Dependency-free local development server for XZBT.
|
||||
*
|
||||
* It deliberately binds only to loopback. It is not part of the distributed
|
||||
* XZBT artifact and cannot establish direct-file Phase 0 feasibility results.
|
||||
*/
|
||||
import { createReadStream, statSync } from 'node:fs';
|
||||
import { createServer } from 'node:http';
|
||||
import { extname, normalize, resolve, sep } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const root = resolve(fileURLToPath(new URL('..', import.meta.url)));
|
||||
const host = '127.0.0.1';
|
||||
const loopbackAddresses = ['127.0.0.1', '::1'];
|
||||
const port = Number.parseInt(process.env.XZBT_DEV_PORT ?? '5173', 10);
|
||||
|
||||
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
||||
throw new Error('XZBT_DEV_PORT must be an integer from 1 through 65535.');
|
||||
}
|
||||
|
||||
const mimeTypes = new Map([
|
||||
['.css', 'text/css; charset=utf-8'],
|
||||
['.html', 'text/html; charset=utf-8'],
|
||||
['.js', 'text/javascript; charset=utf-8'],
|
||||
['.json', 'application/json; charset=utf-8'],
|
||||
['.mjs', 'text/javascript; charset=utf-8'],
|
||||
['.xzbt', 'application/json; charset=utf-8'],
|
||||
]);
|
||||
|
||||
function requestedPath(requestUrl) {
|
||||
const pathname = new URL(requestUrl, `http://${host}:${port}`).pathname;
|
||||
const decodedPath = decodeURIComponent(pathname);
|
||||
if (decodedPath.includes('\0')) return null;
|
||||
if (decodedPath.split('/').some((segment) => segment.startsWith('.'))) return null;
|
||||
|
||||
const relativePath = decodedPath === '/'
|
||||
? 'prototypes/phase0/XZBT-phase0-probe.html'
|
||||
: decodedPath.replace(/^\/+/, '');
|
||||
const candidate = resolve(root, normalize(relativePath));
|
||||
return candidate === root || candidate.startsWith(`${root}${sep}`) ? candidate : null;
|
||||
}
|
||||
|
||||
function handleRequest(request, response) {
|
||||
if (!['GET', 'HEAD'].includes(request.method ?? '')) {
|
||||
response.writeHead(405, { Allow: 'GET, HEAD' }).end();
|
||||
return;
|
||||
}
|
||||
|
||||
let path;
|
||||
try {
|
||||
path = requestedPath(request.url ?? '/');
|
||||
} catch {
|
||||
response.writeHead(400).end('Malformed request path.');
|
||||
return;
|
||||
}
|
||||
if (!path) {
|
||||
response.writeHead(403).end('Path is outside the XZBT workspace.');
|
||||
return;
|
||||
}
|
||||
|
||||
let stats;
|
||||
try {
|
||||
stats = statSync(path);
|
||||
} catch {
|
||||
response.writeHead(404).end('Not found.');
|
||||
return;
|
||||
}
|
||||
if (!stats.isFile()) {
|
||||
response.writeHead(404).end('Not found.');
|
||||
return;
|
||||
}
|
||||
|
||||
response.writeHead(200, {
|
||||
'Content-Length': stats.size,
|
||||
'Content-Type': mimeTypes.get(extname(path).toLowerCase()) ?? 'application/octet-stream',
|
||||
'Cache-Control': 'no-store',
|
||||
'X-Content-Type-Options': 'nosniff',
|
||||
});
|
||||
if (request.method === 'HEAD') response.end();
|
||||
else createReadStream(path).pipe(response);
|
||||
}
|
||||
|
||||
for (const address of loopbackAddresses) {
|
||||
const server = createServer(handleRequest);
|
||||
server.listen(port, address, () => {
|
||||
console.log(`XZBT development server: http://${address}:${port}/`);
|
||||
});
|
||||
}
|
||||
console.log(`Serving workspace root: ${root}`);
|
||||
Reference in New Issue
Block a user