swallowkit 2.0.0-alpha.4 → 2.0.0-alpha.5

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 (76) hide show
  1. package/dist/cli/commands/add-auth.js +37 -6
  2. package/dist/cli/commands/add-auth.js.map +1 -1
  3. package/dist/cli/commands/init.d.ts +2 -0
  4. package/dist/cli/commands/init.d.ts.map +1 -1
  5. package/dist/cli/commands/init.js +3 -1
  6. package/dist/cli/commands/init.js.map +1 -1
  7. package/dist/cli/commands/scaffold.d.ts.map +1 -1
  8. package/dist/cli/commands/scaffold.js +45 -1
  9. package/dist/cli/commands/scaffold.js.map +1 -1
  10. package/dist/cli/index.js +2 -2
  11. package/dist/cli/index.js.map +1 -1
  12. package/dist/core/operations/create-model.d.ts +2 -0
  13. package/dist/core/operations/create-model.d.ts.map +1 -1
  14. package/dist/core/operations/create-model.js +60 -8
  15. package/dist/core/operations/create-model.js.map +1 -1
  16. package/dist/core/project/boundaries.js +3 -3
  17. package/dist/core/project/boundaries.js.map +1 -1
  18. package/dist/core/project/capabilities.d.ts +1 -0
  19. package/dist/core/project/capabilities.d.ts.map +1 -1
  20. package/dist/core/project/capabilities.js +9 -3
  21. package/dist/core/project/capabilities.js.map +1 -1
  22. package/dist/core/project/react-project.js +1 -1
  23. package/dist/core/project/workflows.d.ts.map +1 -1
  24. package/dist/core/project/workflows.js +15 -5
  25. package/dist/core/project/workflows.js.map +1 -1
  26. package/dist/core/scaffold/auth-generator.d.ts.map +1 -1
  27. package/dist/core/scaffold/auth-generator.js +18 -19
  28. package/dist/core/scaffold/auth-generator.js.map +1 -1
  29. package/dist/core/scaffold/native-schema-generator.d.ts +2 -0
  30. package/dist/core/scaffold/native-schema-generator.d.ts.map +1 -1
  31. package/dist/core/scaffold/native-schema-generator.js +16 -1
  32. package/dist/core/scaffold/native-schema-generator.js.map +1 -1
  33. package/dist/core/verify/index.d.ts +4 -3
  34. package/dist/core/verify/index.d.ts.map +1 -1
  35. package/dist/core/verify/index.js +98 -4
  36. package/dist/core/verify/index.js.map +1 -1
  37. package/dist/machine/contracts.d.ts +1 -0
  38. package/dist/machine/contracts.d.ts.map +1 -1
  39. package/dist/machine/errors.d.ts +3 -2
  40. package/dist/machine/errors.d.ts.map +1 -1
  41. package/dist/machine/errors.js +2 -1
  42. package/dist/machine/errors.js.map +1 -1
  43. package/dist/machine/index.d.ts.map +1 -1
  44. package/dist/machine/index.js +3 -2
  45. package/dist/machine/index.js.map +1 -1
  46. package/dist/mcp/index.d.ts +1 -0
  47. package/dist/mcp/index.d.ts.map +1 -1
  48. package/dist/mcp/index.js +19 -4
  49. package/dist/mcp/index.js.map +1 -1
  50. package/package.json +6 -3
  51. package/src/__tests__/__snapshots__/boundaries.test.ts.snap +6 -6
  52. package/src/__tests__/agent-loop-phase34.test.ts +95 -0
  53. package/src/__tests__/generated-project-matrix.test.ts +789 -0
  54. package/src/__tests__/mcp.test.ts +15 -0
  55. package/src/__tests__/multi-auth.test.ts +115 -4
  56. package/src/__tests__/plan-apply.test.ts +111 -0
  57. package/src/cli/commands/add-auth.ts +36 -6
  58. package/src/cli/commands/init.ts +4 -4
  59. package/src/cli/commands/scaffold.ts +65 -1
  60. package/src/cli/index.ts +2 -2
  61. package/src/core/operations/create-model.ts +51 -13
  62. package/src/core/project/agent-templates/AGENTS.md +28 -3
  63. package/src/core/project/agent-templates/CLAUDE.md +4 -0
  64. package/src/core/project/agent-templates/copilot-instructions.md +4 -0
  65. package/src/core/project/agent-templates/shared-models.instructions.md +11 -3
  66. package/src/core/project/boundaries.ts +3 -3
  67. package/src/core/project/capabilities.ts +10 -3
  68. package/src/core/project/react-project.ts +1 -1
  69. package/src/core/project/workflows.ts +15 -5
  70. package/src/core/scaffold/auth-generator.ts +18 -19
  71. package/src/core/scaffold/native-schema-generator.ts +16 -0
  72. package/src/core/verify/index.ts +95 -5
  73. package/src/machine/contracts.ts +1 -0
  74. package/src/machine/errors.ts +4 -2
  75. package/src/machine/index.ts +3 -2
  76. package/src/mcp/index.ts +19 -6
@@ -319,6 +319,20 @@ custom endpoints that derive the scoping key (e.g. `learnerId`) from the
319
319
  verified principal — never from the request body — and restrict the generated
320
320
  write endpoints to admin roles.
321
321
 
322
+ Configure authentication with `plan auth` / `apply auth` **before** declaring
323
+ `authPolicy`. Without authentication, an explicit policy
324
+ blocks plan/apply/scaffold with `auth-policy-without-auth` before file writes;
325
+ follow `nextActions`, then re-plan. Do not remove a required policy just to pass
326
+ generation.
327
+ An empty `authPolicy = {}` means no explicit policy; it does not enable authentication.
328
+
329
+ With named `auth.schemes`, define `auth.authorization.policies` selecting the
330
+ required schemes and roles, then use `authPolicy = { policy: 'adminOnly' }` (or
331
+ read/write policy names). Role-only declarations do not select a named scheme
332
+ and block with `auth-policy-requires-named-policy`. Conversely, single-provider
333
+ `auth.provider` supports role lists only; a named policy (including a named
334
+ `defaultPolicy`) blocks with `auth-named-policy-requires-named-scheme`.
335
+
322
336
  ## Verification Is a Completion Gate
323
337
 
324
338
  Before treating an agent-driven implementation as complete, run SwallowKit
@@ -330,8 +344,17 @@ verification:
330
344
 
331
345
  or the equivalent MCP operation.
332
346
 
333
- Verification covers SwallowKit structural correctness, drift, type correctness,
334
- and configured project checks.
347
+ Default checks are `structure`, `drift`, `typecheck`, `backend-build`, plus
348
+ configured project checks. `backend-build` checks standalone `functions/`:
349
+ C# uses `dotnet build`, TypeScript uses the project's `tsc --noEmit`, and Python
350
+ uses Python 3 `compileall` (syntax only, not imports or type checking), preferring
351
+ `functions/.venv` over PATH. Missing toolchains are `skip` with an explanation;
352
+ a skipped backend check is **not** evidence that the backend works. Restore the
353
+ toolchain and re-run it before claiming backend validation.
354
+
355
+ Root `build` scripts do not replace standalone backend verification. To include
356
+ frontend production builds, use `--checks "structure,drift,typecheck,backend-build,build"`.
357
+ Quote comma-separated values in PowerShell.
335
358
 
336
359
  When verification fails, use the structured failure explanation:
337
360
 
@@ -474,7 +497,9 @@ same generated structure.
474
497
  - Model schema files use `shared/models/{kebab-case}.ts`.
475
498
  - The Zod schema constant and inferred TypeScript type use the same PascalCase
476
499
  name: `export const Todo = z.object(...); export type Todo = z.infer<typeof Todo>`.
477
- - Re-export models from `shared/index.ts`.
500
+ - Re-export models from `shared/index.ts`. create-model and plan/apply scaffold
501
+ append missing exports, including manually authored models. Manual exports
502
+ are allowed; this file has no managed markers and existing content is preserved.
478
503
  - `id` is a required `z.string()`; only `createdAt` / `updatedAt` are optional
479
504
  backend-managed fields.
480
505
  - Models may declare `partitionKey`, `authPolicy`, `displayName`,
@@ -25,6 +25,10 @@ Always-on rules:
25
25
  - Use plan/apply for deterministic generation.
26
26
  - Respect all `requires-human` states and provisioning approval gates.
27
27
  - Run SwallowKit verification before considering implementation complete.
28
+ - Default verification includes `backend-build` for standalone Functions.
29
+ Skipped toolchains are not validation; Python compilation checks syntax only.
30
+ - Configure auth via plan/apply auth before adding `authPolicy`; otherwise
31
+ scaffold blocks before file writes. Scaffold appends missing shared model exports.
28
32
 
29
33
  Task-specific SwallowKit Agent Skills are available under `.github/skills/`.
30
34
  Equivalent agent-agnostic runbooks are available under
@@ -24,6 +24,10 @@ Key rules:
24
24
  - Use plan/apply for deterministic generation.
25
25
  - Respect `requires-human` states and other human approval gates.
26
26
  - Run SwallowKit verification before completion.
27
+ - Default verification includes `backend-build` for standalone Functions.
28
+ Skipped toolchains are not validation; Python compilation checks syntax only.
29
+ - Configure auth via plan/apply auth before adding `authPolicy`; otherwise
30
+ scaffold blocks before file writes. Scaffold appends missing shared model exports.
27
31
 
28
32
  Task-specific SwallowKit Agent Skills live under `.github/skills/`.
29
33
  Agent-agnostic equivalents live under `.swallowkit/workflows/`.
@@ -18,8 +18,9 @@ source-of-truth models, subject to the current result of
18
18
  - `createdAt` and `updatedAt` are **optional** string fields
19
19
  (`z.string().optional()`). The backend manages their values.
20
20
  - Export a `displayName` string for UI display.
21
- - Re-export every model from `shared/index.ts`, outside managed markers where
22
- applicable.
21
+ - create-model and plan/apply scaffold append missing model exports to
22
+ `shared/index.ts`, including manually authored models. Manual exports are
23
+ allowed; there are no managed markers and existing content is preserved.
23
24
  - For relationships, use nested schemas rather than duplicating model shapes.
24
25
 
25
26
  ## Optional model declarations (exact formats)
@@ -30,7 +31,7 @@ These are the only declarations SwallowKit parses from a model file:
30
31
  // Cosmos DB partition key (default '/id'). Must reference a schema field.
31
32
  export const partitionKey = '/learnerId';
32
33
 
33
- // Role-based access control on generated CRUD.
34
+ // Role-based access control on generated CRUD (requires plan/apply auth first).
34
35
  export const authPolicy = { roles: ['admin'] };
35
36
  // or: { read: ['user'], write: ['admin'] } or: { policy: 'adminOnly' }
36
37
 
@@ -48,6 +49,13 @@ export const connectorConfig = {
48
49
  The full machine-readable contract:
49
50
  `{{runCmd}} swallowkit machine inspect capabilities`
50
51
 
52
+ An explicit `authPolicy` without authentication blocks plan/apply/scaffold with
53
+ `auth-policy-without-auth` before writing files. Follow `nextActions` to configure
54
+ auth, then re-plan. Do not drop a required guard to bypass the prerequisite.
55
+ For named `auth.schemes`, use named authorization policies selecting schemes
56
+ and roles, referenced by `policy` or read/write names. Role-only declarations
57
+ block with `auth-policy-requires-named-policy` rather than emitting missing helpers.
58
+
51
59
  After changing a model, do not manually patch generated artifacts.
52
60
 
53
61
  ## After changing a model
@@ -86,7 +86,7 @@ export const OWNERSHIP_POLICIES: OwnershipPolicy[] = [
86
86
  zone: "shared",
87
87
  editPolicy: "edit-outside-markers",
88
88
  howToChange:
89
- "SwallowKit appends only inside marked sections (e.g. `# SwallowKit scaffold registrations`). Edit anywhere outside the markers freely; do not remove the markers.",
89
+ "SwallowKit appends at the file-specific extension point: marked sections (e.g. `# SwallowKit scaffold registrations`) or append-only files such as shared/index.ts. Follow the path rule; preserve existing generated entries and any markers.",
90
90
  },
91
91
  {
92
92
  ownership: "metadata",
@@ -134,8 +134,8 @@ const COMMON_LEADING_RULES: BoundaryPathRule[] = [
134
134
  {
135
135
  pattern: "shared/index.ts",
136
136
  zone: "shared",
137
- editPolicy: "edit-outside-markers",
138
- description: "SwallowKit appends model exports; other exports may be added freely.",
137
+ editPolicy: "free-edit",
138
+ description: "create-model and scaffold append missing model exports while preserving existing content. No managed markers; manual exports are allowed. Keep exports needed by generated code.",
139
139
  },
140
140
  ];
141
141
 
@@ -65,6 +65,7 @@ export interface CapabilitiesContract {
65
65
  defaultChecks: string[];
66
66
  optInChecks: string[];
67
67
  customChecks: string;
68
+ backendBuild: string;
68
69
  };
69
70
  }
70
71
 
@@ -109,6 +110,11 @@ export function inspectCapabilities(): CapabilitiesContract {
109
110
  "Role-based access control metadata applied to the generated CRUD Functions. `roles` guards all operations; `read`/`write` guard per-operation; `policy` references a named authorization policy from swallowkit.config auth.authorization.policies.",
110
111
  format:
111
112
  "export const authPolicy = { roles: ['admin'] };\n// or per-operation:\nexport const authPolicy = { read: ['user'], write: ['admin'] };\n// or a named policy:\nexport const authPolicy = { policy: 'adminOnly' };",
113
+ notes: [
114
+ "An explicit authPolicy without configured authentication blocks plan/apply/generate scaffold with auth-policy-without-auth before any artifacts are written. Run plan auth, apply auth, then re-plan scaffold. An empty {} declaration keeps the existing behavior: no explicit policy.",
115
+ "With named auth.schemes, define auth.authorization.policies selecting schemes and roles, then reference them via policy or read/write names. A role-only declaration cannot select a named scheme and blocks with auth-policy-requires-named-policy; it is supported by legacy single-provider authentication.",
116
+ "Conversely, single-provider authentication (auth.provider) supports role lists only. Referencing a named policy (authPolicy.policy, read/write names, or a named auth.authorization.defaultPolicy) blocks with auth-named-policy-requires-named-scheme; add a named scheme first.",
117
+ ],
112
118
  },
113
119
  {
114
120
  name: "references",
@@ -186,7 +192,7 @@ export function inspectCapabilities(): CapabilitiesContract {
186
192
  "Without authentication, plan scaffold reports user-scoped-data-without-auth when the partition key or an owner-like field (userId, learnerId, ownerId, ...) suggests per-user data.",
187
193
  ],
188
194
  guidance: [
189
- "For multi-user data where users may only touch their own records, add ai-authored custom endpoints that derive the scoping key (e.g. learnerId) from the verified principal — never from the request body — and restrict generated write endpoints to admin roles or disable them.",
195
+ "Configure authentication via plan/apply auth before declaring authPolicy. For multi-user data where users may only touch their own records, add ai-authored custom endpoints that derive the scoping key (e.g. learnerId) from the verified principal — never from the request body — and restrict generated write endpoints to admin roles or disable them.",
190
196
  ],
191
197
  },
192
198
  sharedModels: {
@@ -200,7 +206,7 @@ export function inspectCapabilities(): CapabilitiesContract {
200
206
  "String formats (email/uuid/url), refinements, and transforms are not re-implemented natively; the BFF Zod schemas remain the authoritative validation.",
201
207
  ],
202
208
  buildNote:
203
- "verify typecheck resolves the shared package from its built dist/. SwallowKit rebuilds shared automatically during verify; when typechecking manually, build the shared package first (errors like \"no exported member\" usually mean a stale dist).",
209
+ "create-model and plan/apply scaffold append missing model exports to shared/index.ts, including manually authored models. Manual exports are allowed (no markers); existing content is preserved. verify typecheck resolves the shared package from its built dist/. SwallowKit rebuilds shared automatically during verify; when typechecking manually, build shared first.",
204
210
  },
205
211
  seeding: {
206
212
  createTemplates: "swallowkit create-dev-seeds <environment> — writes dev-seeds/<environment>/<model>.json templates.",
@@ -219,9 +225,10 @@ export function inspectCapabilities(): CapabilitiesContract {
219
225
  "(regular CLI) configure-mcp [--version auto|latest|<semver>] — re-select the MCP launcher version in .mcp.json",
220
226
  ],
221
227
  verifyChecks: {
222
- defaultChecks: ["structure", "drift", "typecheck"],
228
+ defaultChecks: ["structure", "drift", "typecheck", "backend-build"],
223
229
  optInChecks: ["build", "lint", "test"],
224
230
  customChecks: "swallowkit.config verify.checks: [{ id, command, title?, timeoutMs? }]",
231
+ backendBuild: "Runs dotnet build in functions/ (C#), the project tsc --noEmit (TypeScript), or Python 3 compileall (syntax only, using functions/.venv before PATH). Missing toolchains are skip with a reason, not proof of validation. Root build scripts do not replace backend-build; use --checks \"structure,drift,typecheck,backend-build,build\" for both.",
225
232
  },
226
233
  };
227
234
  }
@@ -407,7 +407,7 @@ live in \`functions/local.settings.json\`. Keep their secrets out of source cont
407
407
  \`\`\`sh
408
408
  ${pm} run build
409
409
  ${cmd.exec} swallowkit machine verify project
410
- ${cmd.exec} swallowkit machine verify project --checks structure,drift,typecheck,build
410
+ ${cmd.exec} swallowkit machine verify project --checks "structure,drift,typecheck,backend-build,build"
411
411
  \`\`\`
412
412
 
413
413
  The build compiles shared schemas, checks the SPA, builds static assets, and
@@ -62,6 +62,13 @@ Goal: add a data model and derive CRUD/BFF/UI/infra deterministically.
62
62
  the plan warns \`infra-skipped\`; later run \`${runCmd} swallowkit migrations add-model <name>\` to add only the container migration.
63
63
  - Read \`data.warnings\`. \`user-scoped-data-without-auth\` means generated CRUD would expose every user's documents;
64
64
  add auth (\`swallowkit-add-auth\`) and owner-scoped ai-authored endpoints before exposing the model.
65
+ - \`auth-policy-without-auth\` blocks generation before file writes: configure authentication with plan/apply auth,
66
+ then re-plan. Do not remove a required policy to bypass this prerequisite.
67
+ For named \`auth.schemes\`, define named authorization policies selecting schemes/roles and reference them from
68
+ \`authPolicy.policy\` or read/write names; role-only declarations block with \`auth-policy-requires-named-policy\`.
69
+ Single-provider \`auth.provider\` supports role lists only; named policies block with \`auth-named-policy-requires-named-scheme\`.
70
+ - Scaffold appends missing exports to \`shared/index.ts\` for hand-authored models too.
71
+ Manual exports are allowed; no managed markers exist in that file.
65
72
  4. Apply:
66
73
  \`${runCmd} swallowkit machine apply scaffold --plan <planId>\`
67
74
  5. Verify:
@@ -160,7 +167,7 @@ the scoping key from the verified principal, never from the request body.
160
167
  title: "Verify and repair loop",
161
168
  skillName: "swallowkit-verify-repair",
162
169
  skillDescription:
163
- "Converge a SwallowKit project to a verified state: run structure/drift/typecheck (and build/lint/test) checks, read evidence with explain failure, repair, and repeat. Use after any code or schema change, when verification fails, when the build/lint/tests are broken, or before finishing a task.",
170
+ "Converge a SwallowKit project to a verified state: run structure/drift/typecheck/backend-build (and build/lint/test) checks, read evidence with explain failure, repair, and repeat. Use after any code or schema change, when verification fails, when the build/lint/tests are broken, or before finishing a task.",
164
171
  content: `# Workflow: Verify and repair
165
172
 
166
173
  Goal: converge the project to a verified state after any change.
@@ -168,14 +175,16 @@ Goal: converge the project to a verified state after any change.
168
175
  ## Loop
169
176
 
170
177
  1. \`${runCmd} swallowkit machine verify project\`
171
- - default checks: structure, drift, typecheck (+ project-specific checks from swallowkit.config \`verify.checks\`)
172
- - opt-in heavier checks: \`--checks structure,drift,typecheck,build,lint,test\`
178
+ - default checks: structure, drift, typecheck, backend-build (+ project-specific checks from swallowkit.config \`verify.checks\`)
179
+ - backend-build: standalone C# \`dotnet build\`, TypeScript \`tsc --noEmit\`, Python 3 \`compileall\` (syntax only).
180
+ Missing toolchains are skipped with a reason, not proof of a working backend. Restore them and re-run.
181
+ - opt-in heavier checks: \`--checks "structure,drift,typecheck,backend-build,build,lint,test"\` (quote CSV in PowerShell)
173
182
  2. If \`summary.done\` is false:
174
183
  - \`${runCmd} swallowkit machine explain failure\` → evidence (logTail, findings, violations) and suggestedActions per check.
175
184
  - Repair:
176
185
  - \`drift\` failures → follow each finding's \`repairAction\` (usually re-run plan/apply scaffold).
177
186
  - \`structure\` failures → follow \`suggestedFix\` per violation.
178
- - \`typecheck\`/\`build\`/\`lint\`/\`test\` failures → fix the code in ai-authored files.
187
+ - \`typecheck\`/\`backend-build\`/\`build\`/\`lint\`/\`test\` failures → fix the source code, not generated artifacts.
179
188
  3. Repeat until \`summary.done: true\`.
180
189
 
181
190
  ## Exit criteria
@@ -254,7 +263,8 @@ under \`.github/skills/\` so skills-aware agents load them automatically.
254
263
 
255
264
  - **ai-authored** (write freely): \`shared/models/\`, business logic, custom pages/components, tests.
256
265
  - **deterministic** (never hand-edit): artifacts generated by SwallowKit (see \`.swallowkit/artifacts.json\`) and \`.swallowkit/\` metadata. Change models/config and regenerate via plan/apply.
257
- - **shared** (edit outside markers): \`shared/index.ts\`, plus backend registration files for the configured language (Python: \`functions/function_app.py\`).
266
+ - **shared**: \`shared/index.ts\` allows manual exports (no markers; SwallowKit appends missing entries).
267
+ For Python \`functions/function_app.py\`, edit outside the registration marker.
258
268
  - **infrastructure**: baseline \`infra/\` files are checksum-locked by \`infra/migrations/manifest.json\`; add resources as new migrations (\`swallowkit create-migration <slug>\`, \`swallowkit migrations add-model <model>\`).
259
269
 
260
270
  Full machine-readable contract: \`${runCmd} swallowkit machine inspect boundaries\`
@@ -117,8 +117,7 @@ export function generateNamedAuthRouterCSharp(config: NormalizedAuthConfig): str
117
117
  using System.Text;
118
118
  using System.Text.Json;
119
119
  using Microsoft.Azure.Functions.Worker.Http;
120
- using Functions.Auth.Schemes;
121
- namespace Functions.Auth;
120
+ ${externalCases ? "using Functions.Auth.Schemes;\n" : ""}namespace Functions.Auth;
122
121
  public record AuthPrincipal(string Subject, string Scheme, string Issuer, string[] Roles, IReadOnlyDictionary<string, object?> Claims)
123
122
  {
124
123
  [Obsolete("Use Subject")] public string UserId => Subject;
@@ -171,15 +170,15 @@ ${externalCases}
171
170
  export function generateNamedAuthRouterPython(config: NormalizedAuthConfig): string {
172
171
  const external = Object.entries(config.schemes).filter(([,s])=>s.provider === "external-token");
173
172
  const imports = external.map(([n])=>`from auth.schemes.${schemeSlug(n).replace(/-/g,"_")}.verifier import verify_external_token as verify_${safeIdentifier(n)}, ExternalTokenVerificationError as Error_${safeIdentifier(n)}`).join("\n");
174
- const cases = external.map(([n], index)=>` ${index === 0 ? "if" : "elif"} scheme == ${JSON.stringify(n)}:
175
- try:
176
- value = verify_${safeIdentifier(n)}(bearer)
177
- except Error_${safeIdentifier(n)} as exc:
178
- raise AuthError(503 if exc.identity_provider_unavailable else 401, "Identity provider unavailable" if exc.identity_provider_unavailable else "Invalid token")
179
- except Exception:
180
- raise AuthError(503, "External token verifier unavailable")
181
- if not value.get("subject") or not isinstance(value.get("roles"), list): raise AuthError(401, "Invalid external principal")
182
- principal = {"subject": value["subject"], "scheme": scheme, "issuer": value.get("issuer", scheme), "roles": value["roles"], "claims": value.get("claims", {}), "userId": value["subject"]}`).join("\n");
173
+ const cases = external.map(([n], index)=>` ${index === 0 ? "if" : "elif"} scheme == ${JSON.stringify(n)}:
174
+ try:
175
+ value = verify_${safeIdentifier(n)}(bearer)
176
+ except Error_${safeIdentifier(n)} as exc:
177
+ raise AuthError(503 if exc.identity_provider_unavailable else 401, "Identity provider unavailable" if exc.identity_provider_unavailable else "Invalid token")
178
+ except Exception:
179
+ raise AuthError(503, "External token verifier unavailable")
180
+ if not value.get("subject") or not isinstance(value.get("roles"), list): raise AuthError(401, "Invalid external principal")
181
+ principal = {"subject": value["subject"], "scheme": scheme, "issuer": value.get("issuer", scheme), "roles": value["roles"], "claims": value.get("claims", {}), "userId": value["subject"]}`).join("\n");
183
182
  return `import base64
184
183
  import json
185
184
  import azure.functions as func
@@ -199,14 +198,14 @@ def require_auth(req: func.HttpRequest, policy_name: str):
199
198
  if len(candidates) != 1: raise AuthError(401, "Authentication required" if not candidates else "Ambiguous credentials")
200
199
  scheme = candidates[0]
201
200
  ${cases}
202
- ${external.length ? "elif" : "if"} PROVIDERS[scheme] == "swa":
203
- try:
204
- value=json.loads(base64.b64decode(swa).decode("utf-8")); roles=[r for r in value.get("userRoles",[]) if isinstance(r,str)]
205
- if not value.get("userId") or "authenticated" not in roles: raise ValueError()
206
- if ALLOWED_PROVIDERS[scheme] and value.get("identityProvider") not in ALLOWED_PROVIDERS[scheme]: raise ValueError()
207
- principal={"subject":value["userId"],"scheme":scheme,"issuer":"swa:"+value.get("identityProvider", "unknown"),"roles":roles,"claims":value,"userId":value["userId"]}
208
- except Exception: raise AuthError(401,"Invalid SWA client principal")
209
- else: raise AuthError(401,"Authentication adapter is not configured")
201
+ ${external.length ? "elif" : "if"} PROVIDERS[scheme] == "swa":
202
+ try:
203
+ value=json.loads(base64.b64decode(swa).decode("utf-8")); roles=[r for r in value.get("userRoles",[]) if isinstance(r,str)]
204
+ if not value.get("userId") or "authenticated" not in roles: raise ValueError()
205
+ if ALLOWED_PROVIDERS[scheme] and value.get("identityProvider") not in ALLOWED_PROVIDERS[scheme]: raise ValueError()
206
+ principal={"subject":value["userId"],"scheme":scheme,"issuer":"swa:"+value.get("identityProvider", "unknown"),"roles":roles,"claims":value,"userId":value["userId"]}
207
+ except Exception: raise AuthError(401,"Invalid SWA client principal")
208
+ else: raise AuthError(401,"Authentication adapter is not configured")
210
209
  require_roles(principal, policy.get("roles", [])); return principal
211
210
  def require_roles(principal, roles):
212
211
  if roles and not any(role in principal.get("roles",[]) for role in roles): raise AuthError(403,"Insufficient permissions")
@@ -939,6 +939,21 @@ function ensurePythonCodegenProjectFiles(
939
939
  return requirementsPath;
940
940
  }
941
941
 
942
+ /** Generated backend_models import pydantic v2 at runtime, so the deployed Functions app must install it. */
943
+ export const PYTHON_SCHEMA_RUNTIME_REQUIREMENT = "pydantic>=2.6,<3";
944
+
945
+ function ensurePythonRuntimeRequirement(functionsRoot: string, session: FileOperationSession): void {
946
+ const requirementsPath = path.join(functionsRoot, "requirements.txt");
947
+ const current = session.fileExists(requirementsPath) ? session.readFile(requirementsPath) : "";
948
+ const declared = current.split(/\r?\n/).some((line) => line.trim().split(/[<>=!~;[ ]/)[0].toLowerCase() === "pydantic");
949
+ if (declared) return;
950
+ const prefix = current && !current.endsWith("\n") ? `${current}\n` : current;
951
+ session.writeFile(requirementsPath, `${prefix}${PYTHON_SCHEMA_RUNTIME_REQUIREMENT}\n`, {
952
+ ownership: "extension-point",
953
+ generator: "native-schema",
954
+ });
955
+ }
956
+
942
957
  async function ensureProjectLocalUvCommand(projectRoot: string): Promise<{ command: string; env: NodeJS.ProcessEnv }> {
943
958
  const uvEnv = { ...process.env };
944
959
  const { localUvExecutable } = getProjectLocalUvPaths(projectRoot);
@@ -1082,6 +1097,7 @@ async function generatePythonSchemaArtifacts(
1082
1097
  sourceModel: string
1083
1098
  ): Promise<void> {
1084
1099
  ensurePythonCodegenProjectFiles(functionsRoot, session, sourceModel);
1100
+ ensurePythonRuntimeRequirement(functionsRoot, session);
1085
1101
 
1086
1102
  const packageRoot = path.join(outputDir, "backend_models");
1087
1103
  const modelsRoot = path.join(packageRoot, "models");
@@ -5,16 +5,17 @@
5
5
  * - structure: プロジェクト構造規約 (validateProject)
6
6
  * - drift: 生成物と現在状態の乖離 (detectDrift)
7
7
  * - typecheck: TypeScript 型チェック (tsc --noEmit / typecheck script)
8
+ * - backend-build: standalone Functions のコンパイル (C#/TypeScript) または構文検証 (Python)
8
9
  * - build/lint/test: package.json scripts に同名 script がある場合のみ実行 (なければ skip)
9
10
  * - custom: swallowkit.config の verify.checks で定義されたプロジェクト固有チェック
10
11
  *
11
- * 既定実行セットは structure/drift/typecheck + custom。build/lint/test は --checks で明示指定。
12
+ * 既定実行セットは structure/drift/typecheck/backend-build + custom。build/lint/test は --checks で明示指定。
12
13
  */
13
14
 
14
15
  import * as fs from "fs";
15
16
  import * as path from "path";
16
17
  import { spawnSync } from "child_process";
17
- import { loadConfig } from "../config";
18
+ import { loadConfig, loadConfigFromEnv } from "../config";
18
19
  import { detectDrift, DriftFinding } from "../project/drift";
19
20
  import { saveLastVerifyState } from "../project/state";
20
21
  import { ProjectViolation, validateProject } from "../project/validation";
@@ -60,11 +61,11 @@ export interface VerifyResult {
60
61
  verifiedAt: string;
61
62
  }
62
63
 
63
- export const AVAILABLE_VERIFY_CHECKS = ["structure", "drift", "typecheck", "build", "lint", "test"] as const;
64
+ export const AVAILABLE_VERIFY_CHECKS = ["structure", "drift", "typecheck", "backend-build", "build", "lint", "test"] as const;
64
65
  export type VerifyCheckId = (typeof AVAILABLE_VERIFY_CHECKS)[number];
65
66
 
66
67
  /** 既定で実行される built-in チェック。build/lint/test は明示指定時のみ。 */
67
- export const DEFAULT_VERIFY_CHECKS = ["structure", "drift", "typecheck"] as const;
68
+ export const DEFAULT_VERIFY_CHECKS = ["structure", "drift", "typecheck", "backend-build"] as const;
68
69
 
69
70
  async function runStructureCheck(projectRoot: string): Promise<VerifyCheckResult> {
70
71
  const startedAt = Date.now();
@@ -137,6 +138,7 @@ interface CommandCheckSpec {
137
138
  args: string[];
138
139
  timeoutMs?: number;
139
140
  failAction: string;
141
+ shell?: boolean;
140
142
  }
141
143
 
142
144
  function runCommandCheck(projectRoot: string, spec: CommandCheckSpec): VerifyCheckResult {
@@ -145,9 +147,10 @@ function runCommandCheck(projectRoot: string, spec: CommandCheckSpec): VerifyChe
145
147
  try {
146
148
  const result = spawnSync(spec.command, spec.args, {
147
149
  cwd: projectRoot,
148
- shell: true,
150
+ shell: spec.shell ?? true,
149
151
  encoding: "utf-8",
150
152
  timeout: spec.timeoutMs ?? 5 * 60 * 1000,
153
+ maxBuffer: 10 * 1024 * 1024,
151
154
  });
152
155
 
153
156
  const output = `${result.stdout ?? ""}\n${result.stderr ?? ""}`;
@@ -180,6 +183,11 @@ function runCommandCheck(projectRoot: string, spec: CommandCheckSpec): VerifyChe
180
183
  command: commandLabel,
181
184
  exitCode,
182
185
  logTail: tailLines(output, 50),
186
+ ...(spec.id === "backend-build" ? {
187
+ diagnostics: [...new Set(output.split(/\r?\n/).filter((line) =>
188
+ /\(\d+,\d+\):\s*error\s+\w+/.test(line) ||
189
+ /^\s*(File ".+", line \d+|(?:Sorry: )?(?:Syntax|Indentation|Tab)Error:|\*\*\* Error compiling)/.test(line)))],
190
+ } : {}),
183
191
  },
184
192
  suggestedActions: exitCode === 0 ? [] : [spec.failAction],
185
193
  };
@@ -201,6 +209,86 @@ function runCommandCheck(projectRoot: string, spec: CommandCheckSpec): VerifyChe
201
209
  }
202
210
  }
203
211
 
212
+ function backendUnavailable(reason: string): VerifyCheckResult {
213
+ return {
214
+ id: "backend-build",
215
+ title: "Standalone Functions compilation",
216
+ status: "skip",
217
+ durationMs: 0,
218
+ fixable: false,
219
+ evidence: { logTail: [reason], diagnostics: [reason] },
220
+ suggestedActions: ["Install or restore the backend toolchain, then re-run verify with --checks \"backend-build\". A skipped check is not backend validation."],
221
+ };
222
+ }
223
+
224
+ function runBackendBuildCheck(projectRoot: string): VerifyCheckResult {
225
+ const functionsRoot = path.join(projectRoot, "functions");
226
+ if (!fs.existsSync(functionsRoot)) return backendUnavailable("functions/ not found; no standalone backend was checked.");
227
+ const configPath = ["swallowkit.config.json", "swallowkit.config.js", ".swallowkitrc.json"]
228
+ .map((name) => path.join(projectRoot, name)).find((file) => fs.existsSync(file));
229
+ if (!configPath) return backendUnavailable("SwallowKit configuration not found; backend language is unknown.");
230
+ // Same effective language as generation (getBackendLanguage): env override, then file, invalid -> typescript.
231
+ const configured = loadConfigFromEnv().backend?.language ?? loadConfig(configPath, true, true).backend?.language;
232
+ const language = configured && ["typescript", "csharp", "python"].includes(configured) ? configured : "typescript";
233
+ let command: string;
234
+ let args: string[];
235
+ if (language === "csharp") {
236
+ command = "dotnet";
237
+ args = ["build", "--nologo"];
238
+ const probe = spawnSync(command, ["--version"], { cwd: functionsRoot, encoding: "utf-8", timeout: 30_000 });
239
+ if (probe.error && "code" in probe.error && probe.error.code === "ENOENT") return backendUnavailable("dotnet SDK is not installed or is not on PATH; C# compilation was skipped.");
240
+ } else if (language === "python") {
241
+ const venvPython = process.platform === "win32"
242
+ ? path.join(functionsRoot, ".venv", "Scripts", "python.exe")
243
+ : path.join(functionsRoot, ".venv", "bin", "python");
244
+ const candidates = fs.existsSync(venvPython)
245
+ ? [{ command: venvPython, args: [] as string[] }]
246
+ : process.platform === "win32"
247
+ ? [{ command: "python", args: [] }, { command: "python3", args: [] }, { command: "py", args: ["-3"] }]
248
+ : [{ command: "python3", args: [] }, { command: "python", args: [] }];
249
+ const python = candidates.find((candidate) => {
250
+ const probe = spawnSync(candidate.command, [...candidate.args, "-c", "import sys; sys.exit(0 if sys.version_info.major == 3 else 1)"], {
251
+ cwd: functionsRoot, encoding: "utf-8", timeout: 30_000,
252
+ });
253
+ // Preserve execution errors for an installed interpreter instead of silently falling back.
254
+ return probe.status === 0 || (probe.error && (!("code" in probe.error) || probe.error.code !== "ENOENT"));
255
+ });
256
+ if (!python) return backendUnavailable("Python 3 is not available in functions/.venv or on PATH; Python syntax compilation was skipped.");
257
+ command = python.command;
258
+ // -f: never trust cached bytecode, which can survive same-size edits within one mtime second.
259
+ args = [...python.args, "-m", "compileall", "-q", "-f", "-x", "[/\\\\](?:\\.venv|\\.codegen-venv|node_modules|__pycache__)(?:[/\\\\]|$)", "."];
260
+ } else {
261
+ let tscPath: string;
262
+ try {
263
+ tscPath = require.resolve("typescript/bin/tsc", { paths: [functionsRoot] });
264
+ } catch (error) {
265
+ if (error instanceof Error && "code" in error && error.code === "MODULE_NOT_FOUND") {
266
+ return backendUnavailable("The project's TypeScript compiler is not installed; restore dependencies before verifying functions/.");
267
+ }
268
+ throw error;
269
+ }
270
+ const sharedBuild = runSharedBuildIfPresent(projectRoot);
271
+ if (sharedBuild.ran && !sharedBuild.ok) {
272
+ return {
273
+ id: "backend-build", title: "Standalone Functions compilation", status: sharedBuild.unavailable ? "error" : "fail",
274
+ durationMs: 0, fixable: !sharedBuild.unavailable,
275
+ evidence: { command: sharedBuild.command, logTail: sharedBuild.logTail,
276
+ ...(sharedBuild.unavailable ? { errorCode: "verification-command-unavailable" as const } : {}) },
277
+ suggestedActions: ["Fix the shared package build before compiling the TypeScript backend."],
278
+ };
279
+ }
280
+ command = process.execPath;
281
+ args = [tscPath, "--project", "tsconfig.json", "--noEmit"];
282
+ }
283
+ const result = runCommandCheck(functionsRoot, {
284
+ id: "backend-build", title: "Standalone Functions compilation", command, args, shell: false,
285
+ timeoutMs: 10 * 60 * 1000,
286
+ failAction: "Fix the backend compilation errors in evidence.diagnostics / evidence.logTail, then re-run verify.",
287
+ });
288
+ result.evidence.command = `${result.evidence.command} (in functions/)`;
289
+ return result;
290
+ }
291
+
204
292
  function readPackageScripts(projectRoot: string): Record<string, string> {
205
293
  const packageJsonPath = path.join(projectRoot, "package.json");
206
294
  if (!fs.existsSync(packageJsonPath)) return {};
@@ -430,6 +518,8 @@ export async function runVerify(
430
518
  checks.push(await runDriftCheck(projectRoot));
431
519
  } else if (id === "typecheck") {
432
520
  checks.push(runTypecheckCheck(projectRoot));
521
+ } else if (id === "backend-build") {
522
+ checks.push(runBackendBuildCheck(projectRoot));
433
523
  } else if (id === "build" || id === "lint" || id === "test") {
434
524
  checks.push(runScriptCheck(projectRoot, id));
435
525
  } else {
@@ -23,6 +23,7 @@ export interface MachineErrorResponse {
23
23
  ok: false;
24
24
  command: string;
25
25
  status?: MachineOperationStatus;
26
+ nextActions?: MachineNextAction[];
26
27
  error: {
27
28
  code: string;
28
29
  message: string;
@@ -1,16 +1,18 @@
1
- import type { MachineOperationStatus } from "./contracts";
1
+ import type { MachineNextAction, MachineOperationStatus } from "./contracts";
2
2
 
3
3
  export class MachineCommandError extends Error {
4
4
  readonly code: string;
5
5
  readonly details?: unknown;
6
6
  readonly status?: MachineOperationStatus;
7
+ readonly nextActions?: MachineNextAction[];
7
8
 
8
- constructor(code: string, message: string, details?: unknown, status?: MachineOperationStatus) {
9
+ constructor(code: string, message: string, details?: unknown, status?: MachineOperationStatus, nextActions?: MachineNextAction[]) {
9
10
  super(message);
10
11
  this.name = "MachineCommandError";
11
12
  this.code = code;
12
13
  this.details = details;
13
14
  this.status = status;
15
+ this.nextActions = nextActions;
14
16
  }
15
17
  }
16
18
 
@@ -58,6 +58,7 @@ function writeMachineError(command: string, error: unknown): void {
58
58
  ok: false,
59
59
  command,
60
60
  status: resolveMachineErrorStatus(error),
61
+ ...(error instanceof MachineCommandError && error.nextActions?.length ? { nextActions: error.nextActions } : {}),
61
62
  error: toMachineError(error),
62
63
  };
63
64
  writeMachineResponse(response);
@@ -567,10 +568,10 @@ function createMachineProgram(): Command {
567
568
  const verify = new Command("verify");
568
569
  verify
569
570
  .command("project")
570
- .description("Run verification checks (structure, drift, typecheck) with machine-readable evidence")
571
+ .description("Run verification checks (structure, drift, typecheck, backend-build) with machine-readable evidence")
571
572
  .option(
572
573
  "--checks <ids>",
573
- "Comma-separated check ids to run (structure,drift,typecheck,build,lint,test, plus custom ids from swallowkit.config verify.checks)"
574
+ "Comma-separated check ids to run (structure,drift,typecheck,backend-build,build,lint,test, plus custom ids from swallowkit.config verify.checks); quote CSV values in PowerShell"
574
575
  )
575
576
  .option("--compact", "Suppress info-severity findings/violations from evidence to keep agent-facing output small", false)
576
577
  .action(async (options: { checks?: string; compact?: boolean }) => {
package/src/mcp/index.ts CHANGED
@@ -5,7 +5,7 @@ import * as path from "path";
5
5
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6
6
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7
7
  import * as z from "zod/v4";
8
- import type { MachineNextAction, MachineOperationStatus, MachineResponse } from "../machine/contracts";
8
+ import type { MachineErrorResponse, MachineNextAction, MachineOperationStatus, MachineResponse } from "../machine/contracts";
9
9
  import { getSwallowKitVersion } from "../version";
10
10
  import { detectFromProject, getCommands } from "../utils/package-manager";
11
11
  import {
@@ -25,7 +25,12 @@ interface MachineSuccessPayload<TData> {
25
25
  }
26
26
 
27
27
  type MachineCliRunner = (args: string[]) => Promise<{ stdout: string; stderr: string; exitCode: number }>;
28
- type ToolContentResult = { content: Array<{ type: "text"; text: string }> };
28
+ type ToolContentResult = { content: Array<{ type: "text"; text: string }>; isError?: boolean };
29
+ class MachineCliError extends Error {
30
+ constructor(message: string, readonly response: MachineErrorResponse) {
31
+ super(message);
32
+ }
33
+ }
29
34
  type ToolDefinition = {
30
35
  name: string;
31
36
  description: string;
@@ -73,7 +78,7 @@ async function executeMachineCommand<TData>(
73
78
  const detailsSuffix =
74
79
  parsed.error.details !== undefined ? `\ndetails: ${JSON.stringify(parsed.error.details)}` : "";
75
80
  const statusSuffix = parsed.status ? ` (status: ${parsed.status})` : "";
76
- throw new Error(`[${parsed.error.code}] ${parsed.error.message}${statusSuffix}${detailsSuffix}`);
81
+ throw new MachineCliError(`[${parsed.error.code}] ${parsed.error.message}${statusSuffix}${detailsSuffix}`, parsed);
77
82
  }
78
83
 
79
84
  return parsed;
@@ -308,7 +313,7 @@ export function buildSwallowKitToolDefinitions(
308
313
  },
309
314
  {
310
315
  name: "swallowkit_verify_project",
311
- description: "Run verification checks (structure, drift, typecheck by default; build, lint, test and custom checks from swallowkit.config verify.checks are also available) and return machine-readable evidence. summary.done=true means the project passed all checks. Set compact=true to suppress info-severity findings and keep the output small.",
316
+ description: "Run verification checks (structure, drift, typecheck, backend-build by default; build, lint, test and custom checks from swallowkit.config verify.checks are also available) and return machine-readable evidence. backend-build compiles standalone C#/TypeScript Functions or checks Python syntax; unavailable toolchains are skipped with a reason. summary.done=true means no checks failed, not that skipped checks were validated or runtime/auth flows work. Set compact=true to suppress info-severity findings and keep the output small.",
312
317
  inputSchema: z.object({
313
318
  checks: z.array(z.string()).optional(),
314
319
  compact: z.boolean().optional(),
@@ -452,8 +457,16 @@ export function buildSwallowKitToolDefinitions(
452
457
  return definitions.map((definition) => ({
453
458
  ...definition,
454
459
  handler: async (input: unknown) => {
455
- const result = await definition.handler(input);
456
- return jsonTextContent(withSwallowKitMetadata(JSON.parse(result.content[0].text)));
460
+ try {
461
+ const result = await definition.handler(input);
462
+ return jsonTextContent(withSwallowKitMetadata(JSON.parse(result.content[0].text)));
463
+ } catch (error) {
464
+ // Prerequisite failures carry nextActions; keep them structured so agents can follow them.
465
+ if (error instanceof MachineCliError && error.response.nextActions?.length) {
466
+ return { ...jsonTextContent(withSwallowKitMetadata(error.response)), isError: true };
467
+ }
468
+ throw error;
469
+ }
457
470
  },
458
471
  }));
459
472
  }