swallowkit 1.0.1 → 1.0.3

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.
Files changed (53) hide show
  1. package/dist/__tests__/fixtures.d.ts.map +1 -1
  2. package/dist/__tests__/fixtures.js +3 -0
  3. package/dist/__tests__/fixtures.js.map +1 -1
  4. package/dist/cli/commands/scaffold.d.ts +2 -1
  5. package/dist/cli/commands/scaffold.d.ts.map +1 -1
  6. package/dist/cli/commands/scaffold.js +24 -1
  7. package/dist/cli/commands/scaffold.js.map +1 -1
  8. package/dist/core/operations/file-session.d.ts +9 -0
  9. package/dist/core/operations/file-session.d.ts.map +1 -1
  10. package/dist/core/operations/file-session.js +7 -0
  11. package/dist/core/operations/file-session.js.map +1 -1
  12. package/dist/core/operations/scaffold-apply.d.ts.map +1 -1
  13. package/dist/core/operations/scaffold-apply.js +15 -1
  14. package/dist/core/operations/scaffold-apply.js.map +1 -1
  15. package/dist/core/operations/scaffold-machine.d.ts +2 -0
  16. package/dist/core/operations/scaffold-machine.d.ts.map +1 -1
  17. package/dist/core/operations/scaffold-machine.js.map +1 -1
  18. package/dist/core/operations/scaffold-plan.d.ts +3 -1
  19. package/dist/core/operations/scaffold-plan.d.ts.map +1 -1
  20. package/dist/core/operations/scaffold-plan.js +7 -0
  21. package/dist/core/operations/scaffold-plan.js.map +1 -1
  22. package/dist/core/project/drift.d.ts.map +1 -1
  23. package/dist/core/project/drift.js +6 -4
  24. package/dist/core/project/drift.js.map +1 -1
  25. package/dist/core/scaffold/model-parser.d.ts +10 -1
  26. package/dist/core/scaffold/model-parser.d.ts.map +1 -1
  27. package/dist/core/scaffold/model-parser.js +182 -60
  28. package/dist/core/scaffold/model-parser.js.map +1 -1
  29. package/dist/core/scaffold/native-schema-generator.d.ts.map +1 -1
  30. package/dist/core/scaffold/native-schema-generator.js +61 -13
  31. package/dist/core/scaffold/native-schema-generator.js.map +1 -1
  32. package/dist/core/verify/index.d.ts +2 -0
  33. package/dist/core/verify/index.d.ts.map +1 -1
  34. package/dist/core/verify/index.js +26 -8
  35. package/dist/core/verify/index.js.map +1 -1
  36. package/dist/machine/index.d.ts.map +1 -1
  37. package/dist/machine/index.js +16 -9
  38. package/dist/machine/index.js.map +1 -1
  39. package/package.json +1 -1
  40. package/src/__tests__/agent-loop-phase34.test.ts +35 -4
  41. package/src/__tests__/fixtures.ts +3 -0
  42. package/src/__tests__/model-parser.test.ts +90 -0
  43. package/src/__tests__/plan-apply.test.ts +182 -0
  44. package/src/cli/commands/scaffold.ts +30 -2
  45. package/src/core/operations/file-session.ts +16 -0
  46. package/src/core/operations/scaffold-apply.ts +21 -1
  47. package/src/core/operations/scaffold-machine.ts +2 -0
  48. package/src/core/operations/scaffold-plan.ts +10 -0
  49. package/src/core/project/drift.ts +9 -5
  50. package/src/core/scaffold/model-parser.ts +199 -67
  51. package/src/core/scaffold/native-schema-generator.ts +69 -13
  52. package/src/core/verify/index.ts +29 -8
  53. package/src/machine/index.ts +18 -4
@@ -8,6 +8,14 @@ import * as path from "path";
8
8
  import * as ts from "typescript";
9
9
 
10
10
  import { ModelConnectorConfig, ModelAuthPolicy } from "../../types";
11
+ import { MachineCommandError } from "../../machine/errors";
12
+ import { hashContent } from "../operations/file-session";
13
+
14
+ export type SchemaParserMode = "static-ast" | "dynamic";
15
+
16
+ export interface ModelParserOptions {
17
+ executeNode?: typeof execFileSync;
18
+ }
11
19
 
12
20
  export interface ModelInfo {
13
21
  name: string; // モデル名(例: "Todo")
@@ -22,6 +30,9 @@ export interface ModelInfo {
22
30
  connectorConfig?: ModelConnectorConfig; // コネクタメタデータ(外部データソース用)
23
31
  authPolicy?: ModelAuthPolicy; // 認可ポリシー(ロールベースアクセス制御用)
24
32
  partitionKey: string; // Cosmos DB パーティションキーパス(デフォルト: "/id")
33
+ parserMode: SchemaParserMode;
34
+ parserWarnings: string[];
35
+ semanticFingerprint: string;
25
36
  }
26
37
 
27
38
  export interface FieldInfo {
@@ -55,7 +66,7 @@ export interface NestedSchemaRef {
55
66
  /**
56
67
  * モデルファイルを解析して ModelInfo を返す
57
68
  */
58
- export async function parseModelFile(modelPath: string): Promise<ModelInfo> {
69
+ export async function parseModelFile(modelPath: string, options: ModelParserOptions = {}): Promise<ModelInfo> {
59
70
  if (!fs.existsSync(modelPath)) {
60
71
  throw new Error(`Model file not found: ${modelPath}`);
61
72
  }
@@ -100,7 +111,8 @@ export async function parseModelFile(modelPath: string): Promise<ModelInfo> {
100
111
  const nestedSchemaRefs = detectNestedSchemaRefs(modelPath, content, schemaName);
101
112
 
102
113
  // フィールド情報を抽出(動的インポートを使用)
103
- const fields = await extractFieldsFromSchema(modelPath, schemaName);
114
+ const parsedSchema = await extractFieldsFromSchema(modelPath, schemaName, options.executeNode ?? execFileSync);
115
+ const fields = parsedSchema.fields;
104
116
 
105
117
  // ネストスキーマ情報をフィールドにマージ
106
118
  mergeNestedSchemaInfo(fields, nestedSchemaRefs);
@@ -121,7 +133,7 @@ export async function parseModelFile(modelPath: string): Promise<ModelInfo> {
121
133
  }
122
134
  }
123
135
 
124
- return {
136
+ const modelInfo: ModelInfo = {
125
137
  name: modelName,
126
138
  displayName,
127
139
  schemaName,
@@ -134,7 +146,25 @@ export async function parseModelFile(modelPath: string): Promise<ModelInfo> {
134
146
  partitionKey,
135
147
  ...(connectorConfig ? { connectorConfig } : {}),
136
148
  ...(authPolicy ? { authPolicy } : {}),
149
+ parserMode: parsedSchema.mode,
150
+ parserWarnings: parsedSchema.warnings,
151
+ semanticFingerprint: "",
137
152
  };
153
+ modelInfo.semanticFingerprint = createModelSemanticFingerprint(modelInfo);
154
+ return modelInfo;
155
+ }
156
+
157
+ export function createModelSemanticFingerprint(model: Omit<ModelInfo, "semanticFingerprint" | "filePath">): string {
158
+ return hashContent(JSON.stringify({
159
+ name: model.name,
160
+ displayName: model.displayName,
161
+ schemaName: model.schemaName,
162
+ fields: model.fields,
163
+ nestedSchemaRefs: model.nestedSchemaRefs,
164
+ partitionKey: model.partitionKey,
165
+ connectorConfig: model.connectorConfig,
166
+ authPolicy: model.authPolicy,
167
+ }));
138
168
  }
139
169
 
140
170
  /**
@@ -360,7 +390,128 @@ function compileModelModule(modelPath: string, directory: string, modules = new
360
390
  /**
361
391
  * Zodスキーマから動的にフィールド情報を抽出
362
392
  */
363
- async function extractFieldsFromSchema(modelPath: string, schemaName: string): Promise<FieldInfo[]> {
393
+ interface ParsedSchemaFields {
394
+ fields: FieldInfo[];
395
+ mode: SchemaParserMode;
396
+ warnings: string[];
397
+ }
398
+
399
+ function staticParseFields(modelPath: string, schemaName: string): FieldInfo[] | null {
400
+ const source = ts.createSourceFile(modelPath, fs.readFileSync(modelPath, "utf-8"), ts.ScriptTarget.Latest, true);
401
+ const constants = new Map<string, ts.Expression>();
402
+ const importedObjectSchemas = new Set<string>();
403
+ for (const statement of source.statements) {
404
+ if (ts.isImportDeclaration(statement) && ts.isStringLiteral(statement.moduleSpecifier) &&
405
+ statement.moduleSpecifier.text.startsWith(".") && statement.importClause?.namedBindings &&
406
+ ts.isNamedImports(statement.importClause.namedBindings)) {
407
+ const base = path.resolve(path.dirname(modelPath), statement.moduleSpecifier.text);
408
+ const targetPath = [base, `${base}.ts`, path.join(base, "index.ts")].find(candidate => fs.existsSync(candidate));
409
+ if (targetPath) {
410
+ const targetContent = fs.readFileSync(targetPath, "utf-8");
411
+ for (const element of statement.importClause.namedBindings.elements) {
412
+ const importedName = element.propertyName?.text ?? element.name.text;
413
+ if (new RegExp(`(?:export\\s+)?const\\s+${importedName}\\s*=\\s*z\\.object\\s*\\(`).test(targetContent)) {
414
+ importedObjectSchemas.add(element.name.text);
415
+ }
416
+ }
417
+ }
418
+ }
419
+ if (!ts.isVariableStatement(statement)) continue;
420
+ for (const declaration of statement.declarationList.declarations) {
421
+ if (ts.isIdentifier(declaration.name) && declaration.initializer) constants.set(declaration.name.text, declaration.initializer);
422
+ }
423
+ }
424
+
425
+ const schema = constants.get(schemaName);
426
+ if (!schema || !ts.isCallExpression(schema) || !ts.isPropertyAccessExpression(schema.expression) ||
427
+ schema.expression.name.text !== "object" || schema.arguments.length === 0 || !ts.isObjectLiteralExpression(schema.arguments[0])) {
428
+ return null;
429
+ }
430
+
431
+ const literalValues = (expression: ts.Expression): string[] | null => {
432
+ let value = expression;
433
+ if (ts.isAsExpression(value)) value = value.expression;
434
+ if (ts.isIdentifier(value)) {
435
+ const resolved = constants.get(value.text);
436
+ return resolved ? literalValues(resolved) : null;
437
+ }
438
+ if (!ts.isArrayLiteralExpression(value)) return null;
439
+ const values: string[] = [];
440
+ for (const element of value.elements) {
441
+ if (ts.isStringLiteral(element) || ts.isNumericLiteral(element)) values.push(element.text);
442
+ else return null;
443
+ }
444
+ return values;
445
+ };
446
+
447
+ const parseExpression = (expression: ts.Expression): Omit<FieldInfo, "name"> | null => {
448
+ if (ts.isIdentifier(expression)) {
449
+ const resolved = constants.get(expression.text);
450
+ if (resolved) return parseExpression(resolved);
451
+ return importedObjectSchemas.has(expression.text)
452
+ ? { type: "object", isOptional: false, isNullable: false, isArray: false }
453
+ : null;
454
+ }
455
+ if (!ts.isCallExpression(expression) || !ts.isPropertyAccessExpression(expression.expression)) return null;
456
+ const method = expression.expression.name.text;
457
+ const receiver = expression.expression.expression;
458
+ const wrappers = new Set(["optional", "nullable", "default", "min", "max", "int", "positive", "negative", "nonnegative", "nonpositive", "regex", "email", "url", "uuid", "trim"]);
459
+ if (wrappers.has(method)) {
460
+ const inner = parseExpression(receiver);
461
+ if (!inner) return null;
462
+ if (method === "optional" || method === "default") inner.isOptional = true;
463
+ if (method === "nullable") inner.isNullable = true;
464
+ return inner;
465
+ }
466
+ if (!ts.isIdentifier(receiver) || receiver.text !== "z") return null;
467
+ if (["string", "number", "boolean", "date"].includes(method)) {
468
+ return { type: method, isOptional: false, isNullable: false, isArray: false };
469
+ }
470
+ if (method === "object") return { type: "object", isOptional: false, isNullable: false, isArray: false };
471
+ if (method === "array" && expression.arguments[0]) {
472
+ const element = parseExpression(expression.arguments[0]);
473
+ return element ? { ...element, isArray: true } : null;
474
+ }
475
+ if (method === "enum" && expression.arguments[0]) {
476
+ const enumValues = literalValues(expression.arguments[0]);
477
+ return enumValues ? { type: "string", isOptional: false, isNullable: false, isArray: false, enumValues } : null;
478
+ }
479
+ if (method === "literal" && expression.arguments[0]) {
480
+ const value = expression.arguments[0];
481
+ if (ts.isStringLiteral(value) || ts.isNumericLiteral(value)) {
482
+ return { type: ts.isNumericLiteral(value) ? "number" : "string", isOptional: false, isNullable: false, isArray: false, enumValues: [value.text] };
483
+ }
484
+ if (value.kind === ts.SyntaxKind.TrueKeyword || value.kind === ts.SyntaxKind.FalseKeyword) {
485
+ return { type: "boolean", isOptional: false, isNullable: false, isArray: false, enumValues: [value.kind === ts.SyntaxKind.TrueKeyword ? "true" : "false"] };
486
+ }
487
+ }
488
+ return null;
489
+ };
490
+
491
+ const fields: FieldInfo[] = [];
492
+ for (const property of schema.arguments[0].properties) {
493
+ if (!ts.isPropertyAssignment(property)) return null;
494
+ const name = ts.isIdentifier(property.name) || ts.isStringLiteral(property.name) ? property.name.text : null;
495
+ if (!name) return null;
496
+ const parsed = parseExpression(property.initializer);
497
+ if (!parsed) return null;
498
+ const isForeignKey = name.endsWith("Id") && name.length > 2 && parsed.type === "string";
499
+ fields.push({
500
+ name,
501
+ ...parsed,
502
+ ...(isForeignKey ? { isForeignKey: true, referencedModel: name.slice(0, -2).replace(/^./, char => char.toUpperCase()) } : {}),
503
+ });
504
+ }
505
+ return fields;
506
+ }
507
+
508
+ async function extractFieldsFromSchema(
509
+ modelPath: string,
510
+ schemaName: string,
511
+ executeNode: typeof execFileSync
512
+ ): Promise<ParsedSchemaFields> {
513
+ const staticFields = staticParseFields(modelPath, schemaName);
514
+ if (staticFields) return { fields: staticFields, mode: "static-ast", warnings: [] };
364
515
  const fields: FieldInfo[] = [];
365
516
  let tempDirectory: string | undefined;
366
517
 
@@ -389,7 +540,7 @@ if (isObject) {
389
540
  const shape = typeof schema._def.shape === 'function' ? schema._def.shape() : schema._def.shape;
390
541
  const fields = Object.keys(shape).map(key => {
391
542
  const field = shape[key];
392
- let type = 'string';
543
+ let type = undefined;
393
544
  let isOptional = false;
394
545
  let isNullable = false;
395
546
  let isArray = false;
@@ -457,6 +608,19 @@ if (isObject) {
457
608
  ? fieldDef._def.entries
458
609
  : Object.values(fieldDef._def.entries);
459
610
  }
611
+ } else if (typeName === 'ZodLiteral') {
612
+ const rawLiteralValues = fieldDef.values || fieldDef._def.values ||
613
+ (fieldDef._def.value !== undefined ? [fieldDef._def.value] : undefined);
614
+ const literalValues = rawLiteralValues instanceof Set ? Array.from(rawLiteralValues) : rawLiteralValues;
615
+ if (Array.isArray(literalValues) && literalValues.length > 0) {
616
+ const literalValue = literalValues[0];
617
+ type = typeof literalValue === 'number' ? 'number' : typeof literalValue === 'boolean' ? 'boolean' : 'string';
618
+ enumValues = literalValues.map(value => String(value));
619
+ }
620
+ }
621
+
622
+ if (!type || ((typeName === 'ZodEnum' || typeName === 'ZodNativeEnum') && (!enumValues || enumValues.length === 0))) {
623
+ throw new Error('Unsupported or ambiguous Zod field "' + key + '" (' + (typeName || 'unknown') + ')');
460
624
  }
461
625
 
462
626
  // 外部キー検出: フィールド名が <ModelName>Id のパターンの場合
@@ -473,13 +637,15 @@ if (isObject) {
473
637
  });
474
638
 
475
639
  console.log(JSON.stringify(fields));
640
+ } else {
641
+ throw new Error('Exported schema is not a Zod object');
476
642
  }
477
643
  `;
478
644
 
479
645
  fs.writeFileSync(tempScript, scriptCode, 'utf8');
480
646
 
481
647
  try {
482
- const result = execFileSync(process.execPath, [tempScript], {
648
+ const result = executeNode(process.execPath, [tempScript], {
483
649
  encoding: 'utf8',
484
650
  cwd: projectRoot,
485
651
  });
@@ -499,71 +665,37 @@ if (isObject) {
499
665
  }
500
666
  }
501
667
  } catch (error) {
502
- console.warn('Failed to use dynamic import, falling back to regex parsing');
503
- console.warn('Error:', error);
504
- return extractFieldsWithRegex(modelPath, schemaName);
668
+ const nodeError = error as NodeJS.ErrnoException;
669
+ if (nodeError.code === "EPERM" || nodeError.code === "EACCES" || nodeError.code === "ENOENT") {
670
+ throw new MachineCommandError(
671
+ "schema-evaluation-unavailable",
672
+ `Schema evaluation could not start: ${nodeError.message}`,
673
+ {
674
+ command: process.execPath,
675
+ reason: nodeError.code,
676
+ modelPath,
677
+ diagnostics: ["Dynamic schema evaluation requires permission to start a Node.js child process."],
678
+ nextAction: "Re-run in an environment that permits normal Node.js child process execution.",
679
+ },
680
+ "blocked"
681
+ );
682
+ }
683
+ throw new MachineCommandError(
684
+ "schema-analysis-unsupported",
685
+ `The model could not be analyzed safely without losing schema semantics: ${error instanceof Error ? error.message : String(error)}`,
686
+ {
687
+ command: process.execPath,
688
+ modelPath,
689
+ diagnostics: ["Lossy regex parsing was not used."],
690
+ nextAction: "Use supported static Zod expressions or re-run where dynamic schema evaluation succeeds.",
691
+ },
692
+ "blocked"
693
+ );
505
694
  } finally {
506
695
  if (tempDirectory) fs.rmSync(tempDirectory, { recursive: true, force: true });
507
696
  }
508
697
 
509
- return fields;
510
- }
511
-
512
- /**
513
- * 正規表現でフィールド情報を抽出(フォールバック用)
514
- */
515
- function extractFieldsWithRegex(modelPath: string, schemaName: string): FieldInfo[] {
516
- const fields: FieldInfo[] = [];
517
- const content = fs.readFileSync(modelPath, "utf-8");
518
-
519
- // z.object({ ... }) の内容を抽出(ネストした括弧に対応)
520
- const objectContent = extractObjectContent(content, schemaName);
521
-
522
- if (!objectContent) {
523
- return fields;
524
- }
525
-
526
- // 各フィールドを解析
527
- const fieldRegex = /(\w+)\s*:\s*(z\.\w+)/g;
528
- let match;
529
-
530
- while ((match = fieldRegex.exec(objectContent)) !== null) {
531
- const fieldName = match[1];
532
- const zodDef = match[2];
533
- const zodType = zodDef.split('.')[1];
534
-
535
- const fieldStart = match.index;
536
- const fieldEnd = objectContent.indexOf(',', fieldStart);
537
- const fieldDef = fieldEnd > -1
538
- ? objectContent.substring(fieldStart, fieldEnd)
539
- : objectContent.substring(fieldStart);
540
-
541
- fields.push({
542
- name: fieldName,
543
- type: mapZodTypeToTs(zodType),
544
- isOptional: fieldDef.includes(".optional()"),
545
- isNullable: fieldDef.includes(".nullable()"),
546
- isArray: fieldDef.includes(".array()"),
547
- });
548
- }
549
-
550
- return fields;
551
- }
552
-
553
- /**
554
- * Zod 型を TypeScript 型にマッピング
555
- */
556
- function mapZodTypeToTs(zodType: string): string {
557
- const typeMap: Record<string, string> = {
558
- string: "string",
559
- number: "number",
560
- boolean: "boolean",
561
- date: "Date",
562
- object: "object",
563
- array: "array",
564
- };
565
-
566
- return typeMap[zodType] || "any";
698
+ return { fields, mode: "dynamic", warnings: [] };
567
699
  }
568
700
 
569
701
  /**
@@ -101,6 +101,57 @@ function toPascalIdentifier(value: string): string {
101
101
  return value.charAt(0).toUpperCase() + value.slice(1);
102
102
  }
103
103
 
104
+ interface CSharpEnumMember {
105
+ value: string;
106
+ name: string;
107
+ }
108
+
109
+ const CSHARP_RESERVED_IDENTIFIERS = new Set([
110
+ "abstract", "as", "base", "bool", "break", "byte", "case", "catch", "char", "checked",
111
+ "class", "const", "continue", "decimal", "default", "delegate", "do", "double", "else",
112
+ "enum", "event", "explicit", "extern", "false", "finally", "fixed", "float", "for", "foreach",
113
+ "goto", "if", "implicit", "in", "int", "interface", "internal", "is", "lock", "long",
114
+ "namespace", "new", "null", "object", "operator", "out", "override", "params", "private",
115
+ "protected", "public", "readonly", "ref", "return", "sbyte", "sealed", "short", "sizeof",
116
+ "stackalloc", "static", "string", "struct", "switch", "this", "throw", "true", "try",
117
+ "typeof", "uint", "ulong", "unchecked", "unsafe", "ushort", "using", "virtual", "void",
118
+ "volatile", "while",
119
+ ]);
120
+
121
+ function toCSharpEnumMemberBase(value: string): string {
122
+ const encoded = Array.from(value)
123
+ .map((character) => {
124
+ if (/^[A-Za-z0-9_-]$/.test(character)) return character;
125
+ if (/^\s$/.test(character)) return "-";
126
+ return `-U${character.codePointAt(0)!.toString(16).toUpperCase()}-`;
127
+ })
128
+ .join("");
129
+ let identifier = toPascalIdentifier(encoded).replace(/[^A-Za-z0-9_]/g, "");
130
+ if (!identifier) identifier = "Value";
131
+ if (/^[0-9]/.test(identifier)) identifier = `Value${identifier}`;
132
+ if (CSHARP_RESERVED_IDENTIFIERS.has(identifier)) identifier = `Value${identifier}`;
133
+ return identifier;
134
+ }
135
+
136
+ function buildCSharpEnumMemberMap(values: string[], enumType: string): CSharpEnumMember[] {
137
+ const uniqueValues = [...new Set(values)];
138
+ const bases = uniqueValues.map((value) => ({ value, base: toCSharpEnumMemberBase(value) }));
139
+ const baseCounts = new Map<string, number>();
140
+ for (const entry of bases) baseCounts.set(entry.base, (baseCounts.get(entry.base) ?? 0) + 1);
141
+
142
+ return bases.map(({ value, base }) => {
143
+ let name = base;
144
+ if ((baseCounts.get(base) ?? 0) > 1 || name === enumType) {
145
+ name = `${base}_${Buffer.from(value, "utf-8").toString("hex").toUpperCase() || "Empty"}`;
146
+ }
147
+ return { value, name };
148
+ });
149
+ }
150
+
151
+ function csharpStringLiteral(value: string): string {
152
+ return JSON.stringify(value);
153
+ }
154
+
104
155
  function isDateLikeField(field: Pick<ModelInfo["fields"][number], "name" | "type">): boolean {
105
156
  return field.type === "date" || (field.type === "string" && field.name.toLowerCase().endsWith("at"));
106
157
  }
@@ -256,23 +307,27 @@ namespace SwallowKitBackendModels.Client
256
307
  `;
257
308
  }
258
309
 
259
- function buildCSharpEnumMembers(values: string[]): string {
260
- return values
261
- .map((value, index) => ` ${toPascalIdentifier(value)} = ${index + 1}`)
310
+ function buildCSharpEnumMembers(members: CSharpEnumMember[]): string {
311
+ return members
312
+ .map((member, index) => ` ${member.name} = ${index + 1}`)
262
313
  .join(",\n\n");
263
314
  }
264
315
 
265
- function buildCSharpEnumFromStringCases(field: ModelInfo["fields"][number], nullable: boolean): string {
316
+ function buildCSharpEnumFromStringCases(
317
+ field: ModelInfo["fields"][number],
318
+ members: CSharpEnumMember[],
319
+ nullable: boolean
320
+ ): string {
266
321
  const enumType = `${toPascalIdentifier(field.name)}Enum`;
267
- return field.enumValues!
268
- .map((value) => ` if (value.Equals("${value}", StringComparison.Ordinal))\n return ${enumType}.${toPascalIdentifier(value)};`)
322
+ return members
323
+ .map((member) => ` if (value.Equals(${csharpStringLiteral(member.value)}, StringComparison.Ordinal))\n return ${enumType}.${member.name};`)
269
324
  .join("\n\n") + (nullable ? `\n\n return null;` : `\n\n throw new NotImplementedException($"Could not convert value to type ${enumType}: '{value}'");`);
270
325
  }
271
326
 
272
- function buildCSharpEnumToJsonCases(field: ModelInfo["fields"][number]): string {
327
+ function buildCSharpEnumToJsonCases(field: ModelInfo["fields"][number], members: CSharpEnumMember[]): string {
273
328
  const enumType = `${toPascalIdentifier(field.name)}Enum`;
274
- return field.enumValues!
275
- .map((value) => ` if (value == ${enumType}.${toPascalIdentifier(value)})\n return "${value}";`)
329
+ return members
330
+ .map((member) => ` if (value == ${enumType}.${member.name})\n return ${csharpStringLiteral(member.value)};`)
276
331
  .join("\n\n");
277
332
  }
278
333
 
@@ -295,28 +350,29 @@ function generateLegacyCompatibleCSharpModelSource(model: ModelInfo): string {
295
350
  .filter((field) => field.enumValues?.length)
296
351
  .map((field) => {
297
352
  const enumType = `${toPascalIdentifier(field.name)}Enum`;
353
+ const enumMembers = buildCSharpEnumMemberMap(field.enumValues!, enumType);
298
354
  return ` /// <summary>
299
355
  /// Defines ${toPascalIdentifier(field.name)}
300
356
  /// </summary>
301
357
  [JsonConverter(typeof(JsonStringEnumConverter))]
302
358
  public enum ${enumType}
303
359
  {
304
- ${buildCSharpEnumMembers(field.enumValues!)}
360
+ ${buildCSharpEnumMembers(enumMembers)}
305
361
  }
306
362
 
307
363
  public static ${enumType} ${enumType}FromString(string value)
308
364
  {
309
- ${buildCSharpEnumFromStringCases(field, false)}
365
+ ${buildCSharpEnumFromStringCases(field, enumMembers, false)}
310
366
  }
311
367
 
312
368
  public static ${enumType}? ${enumType}FromStringOrDefault(string value)
313
369
  {
314
- ${buildCSharpEnumFromStringCases(field, true)}
370
+ ${buildCSharpEnumFromStringCases(field, enumMembers, true)}
315
371
  }
316
372
 
317
373
  public static string ${enumType}ToJsonValue(${enumType}? value)
318
374
  {
319
- ${buildCSharpEnumToJsonCases(field)}
375
+ ${buildCSharpEnumToJsonCases(field, enumMembers)}
320
376
 
321
377
  throw new NotImplementedException($"Value could not be handled: '{value}'");
322
378
  }`;
@@ -31,6 +31,8 @@ export interface VerifyCheckEvidence {
31
31
  logTail?: string[];
32
32
  /** --compact で抑制された info-severity findings の件数。 */
33
33
  suppressedInfoFindings?: number;
34
+ errorCode?: "verification-command-unavailable";
35
+ diagnostics?: string[];
34
36
  }
35
37
 
36
38
  export interface VerifyCheckResult {
@@ -158,8 +160,13 @@ function runCommandCheck(projectRoot: string, spec: CommandCheckSpec): VerifyChe
158
160
  status: "error",
159
161
  durationMs: Date.now() - startedAt,
160
162
  fixable: false,
161
- evidence: { command: commandLabel, logTail: [result.error.message] },
162
- suggestedActions: [`Ensure "${commandLabel}" can run in this project.`],
163
+ evidence: {
164
+ command: commandLabel,
165
+ logTail: [result.error.message],
166
+ errorCode: "verification-command-unavailable",
167
+ diagnostics: [`Unable to start the verification command: ${result.error.message}`],
168
+ },
169
+ suggestedActions: [`Re-run "${commandLabel}" in an environment that permits child process execution.`],
163
170
  };
164
171
  }
165
172
 
@@ -183,8 +190,13 @@ function runCommandCheck(projectRoot: string, spec: CommandCheckSpec): VerifyChe
183
190
  status: "error",
184
191
  durationMs: Date.now() - startedAt,
185
192
  fixable: false,
186
- evidence: { command: commandLabel, logTail: [error instanceof Error ? error.message : String(error)] },
187
- suggestedActions: [],
193
+ evidence: {
194
+ command: commandLabel,
195
+ logTail: [error instanceof Error ? error.message : String(error)],
196
+ errorCode: "verification-command-unavailable",
197
+ diagnostics: ["The verification command could not be started in this environment."],
198
+ },
199
+ suggestedActions: [`Re-run "${commandLabel}" in an environment that permits child process execution.`],
188
200
  };
189
201
  }
190
202
  }
@@ -225,10 +237,17 @@ function runTypecheckCheck(projectRoot: string): VerifyCheckResult {
225
237
  status: "fail",
226
238
  durationMs: Date.now() - startedAt,
227
239
  fixable: true,
228
- evidence: { command: sharedBuild.command, logTail: sharedBuild.logTail },
229
- suggestedActions: [
230
- "Fix the shared package build errors first; typecheck resolves the shared package from its built dist/.",
231
- ],
240
+ evidence: {
241
+ command: sharedBuild.command,
242
+ logTail: sharedBuild.logTail,
243
+ ...(sharedBuild.unavailable ? {
244
+ errorCode: "verification-command-unavailable" as const,
245
+ diagnostics: ["The shared package build command could not be started in this environment."],
246
+ } : {}),
247
+ },
248
+ suggestedActions: sharedBuild.unavailable
249
+ ? ["Re-run verification in an environment that permits child process execution."]
250
+ : ["Fix the shared package build errors first; typecheck resolves the shared package from its built dist/."],
232
251
  };
233
252
  }
234
253
 
@@ -264,6 +283,7 @@ interface SharedBuildOutcome {
264
283
  ok: boolean;
265
284
  command?: string;
266
285
  logTail?: string[];
286
+ unavailable?: boolean;
267
287
  }
268
288
 
269
289
  /** shared/package.json に build script があれば実行する (なければ no-op)。 */
@@ -292,6 +312,7 @@ function runSharedBuildIfPresent(projectRoot: string): SharedBuildOutcome {
292
312
  ok,
293
313
  command: `${command} run build (in shared/)`,
294
314
  logTail: ok ? undefined : tailLines(result.error ? `${output}\n${result.error.message}` : output, 50),
315
+ unavailable: Boolean(result.error),
295
316
  };
296
317
  }
297
318
 
@@ -548,13 +548,27 @@ function createMachineProgram(): Command {
548
548
  "verify-project",
549
549
  async () => {
550
550
  const result = await runVerify(options.checks ? options.checks.split(",") : undefined);
551
+ const unavailable = result.checks.find(
552
+ (check) => check.evidence.errorCode === "verification-command-unavailable"
553
+ );
554
+ if (unavailable) {
555
+ throw new MachineCommandError(
556
+ "verification-command-unavailable",
557
+ `Verification could not start "${unavailable.evidence.command ?? unavailable.id}" in this environment.`,
558
+ {
559
+ check: unavailable.id,
560
+ command: unavailable.evidence.command,
561
+ diagnostics: unavailable.evidence.diagnostics ?? unavailable.evidence.logTail,
562
+ nextAction: "Re-run verification in an environment that permits normal child process execution.",
563
+ },
564
+ "blocked"
565
+ );
566
+ }
551
567
  return options.compact ? compactVerifyResult(result) : result;
552
568
  },
553
569
  (result) => ({
554
- status: result.summary.done ? "complete" : "in-progress",
555
- nextActions: result.summary.done
556
- ? []
557
- : [
570
+ status: result.summary.done ? "complete" : "failed",
571
+ nextActions: result.summary.done ? [] : [
558
572
  {
559
573
  command: "swallowkit machine explain failure",
560
574
  description: "Get evidence and suggested actions for the failed checks.",