Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 21 additions & 21 deletions .claude/hooks/oxlint-fix.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,36 +6,36 @@ const projectDir = process.env.CLAUDE_PROJECT_DIR ?? process.cwd();

const file = await readFilePath();
if (!file || !/\.(tsx?|jsx?|mjs|cjs)$/.test(file)) {
process.exit(0);
process.exit(0);
}

try {
execFileSync('pnpm', ['exec', 'oxlint', '--fix', file], {
cwd: projectDir,
stdio: 'ignore',
});
execFileSync('pnpm', ['exec', 'oxlint', '--fix', file], {
cwd: projectDir,
stdio: 'ignore',
});
} catch {
// 整形に失敗してもツール結果はブロックしない。
// 整形に失敗してもツール結果はブロックしない。
}
process.exit(0);

async function readFilePath() {
const raw = await readStdin();
try {
return JSON.parse(raw)?.tool_input?.file_path ?? '';
} catch {
return '';
}
const raw = await readStdin();
try {
return JSON.parse(raw)?.tool_input?.file_path ?? '';
} catch {
return '';
}
}

function readStdin() {
return new Promise((resolve) => {
let data = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', (chunk) => {
data += chunk;
});
process.stdin.on('end', () => resolve(data));
process.stdin.on('error', () => resolve(''));
});
return new Promise((resolve) => {
let data = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', (chunk) => {
data += chunk;
});
process.stdin.on('end', () => resolve(data));
process.stdin.on('error', () => resolve(''));
});
}
50 changes: 25 additions & 25 deletions .claude/hooks/protect-deps.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,41 +8,41 @@ const file = await readFilePath();
const name = basename(file);

if (name === 'pnpm-lock.yaml') {
console.error(
'pnpm-lock.yaml は直接編集しないでください。依存は package.json / catalog を変更し、pnpm install で lockfile を再生成してください。',
);
process.exit(2); // ブロック。stderr が Claude に渡る。
console.error(
'pnpm-lock.yaml は直接編集しないでください。依存は package.json / catalog を変更し、pnpm install で lockfile を再生成してください。',
);
process.exit(2); // ブロック。stderr が Claude に渡る。
}

if (name === 'pnpm-workspace.yaml' || name === 'Dockerfile') {
process.stdout.write(
JSON.stringify({
systemMessage:
process.stdout.write(
JSON.stringify({
systemMessage:
'[native-deps-guard] catalog / Dockerfile を編集します。tfjs-node 4.22(N-API v8) / Node22 / avx2+fma / glibc 固定との整合を /native-deps-guard で確認してください。',
}),
);
process.exit(0); // 許可(リマインドのみ)。
}),
);
process.exit(0); // 許可(リマインドのみ)。
}

process.exit(0);

async function readFilePath() {
const raw = await readStdin();
try {
return JSON.parse(raw)?.tool_input?.file_path ?? '';
} catch {
return '';
}
const raw = await readStdin();
try {
return JSON.parse(raw)?.tool_input?.file_path ?? '';
} catch {
return '';
}
}

function readStdin() {
return new Promise((resolve) => {
let data = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', (chunk) => {
data += chunk;
});
process.stdin.on('end', () => resolve(data));
process.stdin.on('error', () => resolve(''));
});
return new Promise((resolve) => {
let data = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', (chunk) => {
data += chunk;
});
process.stdin.on('end', () => resolve(data));
process.stdin.on('error', () => resolve(''));
});
}
16 changes: 16 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
root = true

[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = tab
indent_size = 2
tab_width = 2

[*.{json,jsonc,json5,yml,yaml,md}]
indent_style = space

[*.md]
trim_trailing_whitespace = false
92 changes: 46 additions & 46 deletions apps/server/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,50 +14,50 @@ import type { AppDeps, AppEnv } from './types.js';
* 上限超過の非 multipart ボディは 415 ではなく 413 になる(これが正本契約)。
*/
export function createApp(deps: AppDeps): Hono<AppEnv> {
const app = new Hono<AppEnv>();

app.use('*', requestContext(deps));

// ヘルスチェック。auth より前に登録して認証不要で公開する(漏れる情報は readiness のみ)。
// 200 ok = 受付可能。503 unavailable = 受付不能で、reason に理由を載せる:
// - model_unavailable: 起動時にモデルロード失敗(CPU 非対応 / import・load 失敗)。
// - inference_saturated: 全スロットが推論タイムアウト後も解放されない(hung)=容量ゼロ。
// orchestrator の liveness probe にこれを拾わせ、プロセス再起動で回復させる想定。
// 推論経路には一切触れない読み取り専用エンドポイント。
app.get('/health', (c) => {
if (!deps.classifier.available) {
return c.json({ status: 'unavailable' as const, reason: 'model_unavailable' as const }, 503);
}
if (deps.health.saturated) {
return c.json({ status: 'unavailable' as const, reason: 'inference_saturated' as const }, 503);
}
return c.json({ status: 'ok' as const }, 200);
});

app.use('*', auth(deps));
app.use(
'/v1/detect-images',
bodyLimit({
maxSize: deps.config.maxBodySize,
onError: (c) =>
c.json(
{
success: false as const,
error: { code: 'REQUEST_TOO_LARGE' as const, message: `body exceeds ${deps.config.maxBodySize} bytes` },
},
413,
),
}),
);

app.post('/v1/detect-images', detectImagesRoute(deps));

app.notFound((c) => fail(c, 'INVALID_REQUEST', `no route for ${c.req.method} ${c.req.path}`));

app.onError((err, c) => {
deps.logger.error({ err }, 'unhandled error in request pipeline');
return fail(c, 'DETECTION_FAILED', 'internal server error');
});

return app;
const app = new Hono<AppEnv>();

app.use('*', requestContext(deps));

// ヘルスチェック。auth より前に登録して認証不要で公開する(漏れる情報は readiness のみ)。
// 200 ok = 受付可能。503 unavailable = 受付不能で、reason に理由を載せる:
// - model_unavailable: 起動時にモデルロード失敗(CPU 非対応 / import・load 失敗)。
// - inference_saturated: 全スロットが推論タイムアウト後も解放されない(hung)=容量ゼロ。
// orchestrator の liveness probe にこれを拾わせ、プロセス再起動で回復させる想定。
// 推論経路には一切触れない読み取り専用エンドポイント。
app.get('/health', (c) => {
if (!deps.classifier.available) {
return c.json({ status: 'unavailable' as const, reason: 'model_unavailable' as const }, 503);
}
if (deps.health.saturated) {
return c.json({ status: 'unavailable' as const, reason: 'inference_saturated' as const }, 503);
}
return c.json({ status: 'ok' as const }, 200);
});

app.use('*', auth(deps));
app.use(
'/v1/detect-images',
bodyLimit({
maxSize: deps.config.maxBodySize,
onError: (c) =>
c.json(
{
success: false as const,
error: { code: 'REQUEST_TOO_LARGE' as const, message: `body exceeds ${deps.config.maxBodySize} bytes` },
},
413,
),
}),
);

app.post('/v1/detect-images', detectImagesRoute(deps));

app.notFound((c) => fail(c, 'INVALID_REQUEST', `no route for ${c.req.method} ${c.req.path}`));

app.onError((err, c) => {
deps.logger.error({ err }, 'unhandled error in request pipeline');
return fail(c, 'DETECTION_FAILED', 'internal server error');
});

return app;
}
Loading
Loading