swallowkit 2.0.0-alpha.4 → 2.0.0-alpha.6

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 (113) 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/provision.d.ts +2 -1
  8. package/dist/cli/commands/provision.d.ts.map +1 -1
  9. package/dist/cli/commands/provision.js +66 -4
  10. package/dist/cli/commands/provision.js.map +1 -1
  11. package/dist/cli/commands/scaffold.d.ts.map +1 -1
  12. package/dist/cli/commands/scaffold.js +45 -1
  13. package/dist/cli/commands/scaffold.js.map +1 -1
  14. package/dist/cli/index.d.ts.map +1 -1
  15. package/dist/cli/index.js +15 -2
  16. package/dist/cli/index.js.map +1 -1
  17. package/dist/core/operations/create-model.d.ts +2 -0
  18. package/dist/core/operations/create-model.d.ts.map +1 -1
  19. package/dist/core/operations/create-model.js +60 -8
  20. package/dist/core/operations/create-model.js.map +1 -1
  21. package/dist/core/operations/provision-deployment.d.ts +45 -0
  22. package/dist/core/operations/provision-deployment.d.ts.map +1 -0
  23. package/dist/core/operations/provision-deployment.js +151 -0
  24. package/dist/core/operations/provision-deployment.js.map +1 -0
  25. package/dist/core/operations/provision-operations.d.ts +39 -5
  26. package/dist/core/operations/provision-operations.d.ts.map +1 -1
  27. package/dist/core/operations/provision-operations.js +609 -103
  28. package/dist/core/operations/provision-operations.js.map +1 -1
  29. package/dist/core/project/boundaries.js +3 -3
  30. package/dist/core/project/boundaries.js.map +1 -1
  31. package/dist/core/project/capabilities.d.ts +9 -0
  32. package/dist/core/project/capabilities.d.ts.map +1 -1
  33. package/dist/core/project/capabilities.js +20 -6
  34. package/dist/core/project/capabilities.js.map +1 -1
  35. package/dist/core/project/react-project.js +1 -1
  36. package/dist/core/project/state.d.ts +7 -0
  37. package/dist/core/project/state.d.ts.map +1 -1
  38. package/dist/core/project/state.js +86 -0
  39. package/dist/core/project/state.js.map +1 -1
  40. package/dist/core/project/workflows.d.ts.map +1 -1
  41. package/dist/core/project/workflows.js +43 -7
  42. package/dist/core/project/workflows.js.map +1 -1
  43. package/dist/core/scaffold/auth-generator.d.ts.map +1 -1
  44. package/dist/core/scaffold/auth-generator.js +18 -19
  45. package/dist/core/scaffold/auth-generator.js.map +1 -1
  46. package/dist/core/scaffold/native-schema-generator.d.ts +2 -0
  47. package/dist/core/scaffold/native-schema-generator.d.ts.map +1 -1
  48. package/dist/core/scaffold/native-schema-generator.js +16 -1
  49. package/dist/core/scaffold/native-schema-generator.js.map +1 -1
  50. package/dist/core/verify/index.d.ts +4 -3
  51. package/dist/core/verify/index.d.ts.map +1 -1
  52. package/dist/core/verify/index.js +98 -4
  53. package/dist/core/verify/index.js.map +1 -1
  54. package/dist/machine/contracts.d.ts +1 -0
  55. package/dist/machine/contracts.d.ts.map +1 -1
  56. package/dist/machine/errors.d.ts +3 -2
  57. package/dist/machine/errors.d.ts.map +1 -1
  58. package/dist/machine/errors.js +2 -1
  59. package/dist/machine/errors.js.map +1 -1
  60. package/dist/machine/index.d.ts.map +1 -1
  61. package/dist/machine/index.js +20 -13
  62. package/dist/machine/index.js.map +1 -1
  63. package/dist/mcp/index.d.ts +3 -2
  64. package/dist/mcp/index.d.ts.map +1 -1
  65. package/dist/mcp/index.js +76 -15
  66. package/dist/mcp/index.js.map +1 -1
  67. package/dist/utils/azure-cli.d.ts +16 -4
  68. package/dist/utils/azure-cli.d.ts.map +1 -1
  69. package/dist/utils/azure-cli.js +166 -4
  70. package/dist/utils/azure-cli.js.map +1 -1
  71. package/dist/utils/azure-diagnostics.d.ts +16 -0
  72. package/dist/utils/azure-diagnostics.d.ts.map +1 -0
  73. package/dist/utils/azure-diagnostics.js +68 -0
  74. package/dist/utils/azure-diagnostics.js.map +1 -0
  75. package/package.json +6 -3
  76. package/src/__tests__/__snapshots__/boundaries.test.ts.snap +6 -6
  77. package/src/__tests__/agent-instructions.test.ts +3 -0
  78. package/src/__tests__/agent-loop-phase34.test.ts +114 -8
  79. package/src/__tests__/azure-cli.test.ts +5 -2
  80. package/src/__tests__/fixtures/az wrapper.cmd +2 -0
  81. package/src/__tests__/fixtures/azure-cli.cjs +130 -0
  82. package/src/__tests__/fixtures/provision-runner.cjs +28 -0
  83. package/src/__tests__/generated-project-matrix.test.ts +789 -0
  84. package/src/__tests__/mcp.test.ts +71 -0
  85. package/src/__tests__/multi-auth.test.ts +115 -4
  86. package/src/__tests__/plan-apply.test.ts +111 -0
  87. package/src/__tests__/provision-runtime.test.ts +562 -0
  88. package/src/cli/commands/add-auth.ts +36 -6
  89. package/src/cli/commands/init.ts +4 -4
  90. package/src/cli/commands/provision.ts +69 -5
  91. package/src/cli/commands/scaffold.ts +65 -1
  92. package/src/cli/index.ts +12 -2
  93. package/src/core/operations/create-model.ts +51 -13
  94. package/src/core/operations/provision-deployment.ts +167 -0
  95. package/src/core/operations/provision-operations.ts +597 -145
  96. package/src/core/project/agent-templates/AGENTS.md +46 -3
  97. package/src/core/project/agent-templates/CLAUDE.md +9 -0
  98. package/src/core/project/agent-templates/copilot-instructions.md +10 -0
  99. package/src/core/project/agent-templates/shared-models.instructions.md +11 -3
  100. package/src/core/project/boundaries.ts +3 -3
  101. package/src/core/project/capabilities.ts +29 -6
  102. package/src/core/project/react-project.ts +1 -1
  103. package/src/core/project/state.ts +70 -0
  104. package/src/core/project/workflows.ts +43 -7
  105. package/src/core/scaffold/auth-generator.ts +18 -19
  106. package/src/core/scaffold/native-schema-generator.ts +16 -0
  107. package/src/core/verify/index.ts +95 -5
  108. package/src/machine/contracts.ts +1 -0
  109. package/src/machine/errors.ts +4 -2
  110. package/src/machine/index.ts +23 -16
  111. package/src/mcp/index.ts +68 -20
  112. package/src/utils/azure-cli.ts +167 -6
  113. package/src/utils/azure-diagnostics.ts +75 -0
@@ -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
 
@@ -378,6 +401,24 @@ Use the `swallowkit-provision` Agent Skill or runbook and never bypass its
378
401
  approval gate. A broad request to deploy does not itself grant the explicit
379
402
  approval required by that workflow.
380
403
 
404
+ Provision applies Bicep once and tracks the original Azure deployment with a
405
+ bounded wait. A local timeout/error, output truncation, or Ctrl+C is not proof
406
+ that Azure failed. `in-progress` means Azure is Running/Accepted; `unknown`
407
+ means its result could not be read; `partial` means Azure succeeded but
408
+ tag/ledger completion is pending. Never re-submit Bicep, reconcile, adopt,
409
+ cancel, or delete just to repair waiting or migration bookkeeping.
410
+
411
+ Retain `.swallowkit/state/provision/` execution checkpoints. Use
412
+ `{{runCmd}} swallowkit machine inspect provision --plan <runId> --wait-seconds 300`
413
+ for read-only status/wait, and after explicit human approval use
414
+ `{{runCmd}} swallowkit machine apply provision --plan <runId> --approve`
415
+ to resume. `runId` equals the original `planId`; fingerprints, subscription,
416
+ deployment identity and migration checks remain in force. Only
417
+ `status: "complete"` / `outcome: "succeeded"` means Azure Succeeded plus verified
418
+ tags and local ledger. Do not print deployment output secrets or raw `az --debug`.
419
+ For older untracked baselines use `plan provision --resume-deployment <name>`
420
+ with original target/regions; it blocks unless ARM/parameter evidence matches.
421
+
381
422
  ## SwallowKit Agent Skills and Runbooks
382
423
 
383
424
  SwallowKit installs task-specific Agent Skills under:
@@ -474,7 +515,9 @@ same generated structure.
474
515
  - Model schema files use `shared/models/{kebab-case}.ts`.
475
516
  - The Zod schema constant and inferred TypeScript type use the same PascalCase
476
517
  name: `export const Todo = z.object(...); export type Todo = z.infer<typeof Todo>`.
477
- - Re-export models from `shared/index.ts`.
518
+ - Re-export models from `shared/index.ts`. create-model and plan/apply scaffold
519
+ append missing exports, including manually authored models. Manual exports
520
+ are allowed; this file has no managed markers and existing content is preserved.
478
521
  - `id` is a required `z.string()`; only `createdAt` / `updatedAt` are optional
479
522
  backend-managed fields.
480
523
  - Models may declare `partitionKey`, `authPolicy`, `displayName`,
@@ -24,7 +24,16 @@ Always-on rules:
24
24
  after apply).
25
25
  - Use plan/apply for deterministic generation.
26
26
  - Respect all `requires-human` states and provisioning approval gates.
27
+ - Provision timeouts/Ctrl+C do not prove Azure failed. Inspect the original
28
+ execution checkpoint with `machine inspect provision --plan <runId>`; resume
29
+ `machine apply provision --plan <runId> --approve` only after explicit human
30
+ approval. Preserve checkpoints; never re-submit Bicep/reconcile/cancel/delete
31
+ to repair waiting or tag/ledger failures. Do not print deployment output secrets.
27
32
  - Run SwallowKit verification before considering implementation complete.
33
+ - Default verification includes `backend-build` for standalone Functions.
34
+ Skipped toolchains are not validation; Python compilation checks syntax only.
35
+ - Configure auth via plan/apply auth before adding `authPolicy`; otherwise
36
+ scaffold blocks before file writes. Scaffold appends missing shared model exports.
28
37
 
29
38
  Task-specific SwallowKit Agent Skills are available under `.github/skills/`.
30
39
  Equivalent agent-agnostic runbooks are available under
@@ -23,7 +23,17 @@ Key rules:
23
23
  after apply).
24
24
  - Use plan/apply for deterministic generation.
25
25
  - Respect `requires-human` states and other human approval gates.
26
+ - Provision timeouts/Ctrl+C do not prove Azure failed. Read `status`, `outcome`,
27
+ and `nextActions`; inspect the original run with `machine inspect provision
28
+ --plan <runId>`, then resume `machine apply provision --plan <runId> --approve`
29
+ only after explicit human approval. Preserve execution checkpoints; never
30
+ re-submit Bicep or use reconcile/cancel/delete to repair waiting or tag/ledger
31
+ failures. Do not print deployment output secrets.
26
32
  - Run SwallowKit verification before completion.
33
+ - Default verification includes `backend-build` for standalone Functions.
34
+ Skipped toolchains are not validation; Python compilation checks syntax only.
35
+ - Configure auth via plan/apply auth before adding `authPolicy`; otherwise
36
+ scaffold blocks before file writes. Scaffold appends missing shared model exports.
27
37
 
28
38
  Task-specific SwallowKit Agent Skills live under `.github/skills/`.
29
39
  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
 
@@ -61,10 +61,19 @@ export interface CapabilitiesContract {
61
61
  notes: string[];
62
62
  };
63
63
  machineCommands: string[];
64
+ provisioning: {
65
+ execution: string;
66
+ completion: string;
67
+ tracking: string;
68
+ recovery: string;
69
+ diagnostics: string;
70
+ approval: string;
71
+ };
64
72
  verifyChecks: {
65
73
  defaultChecks: string[];
66
74
  optInChecks: string[];
67
75
  customChecks: string;
76
+ backendBuild: string;
68
77
  };
69
78
  }
70
79
 
@@ -109,6 +118,11 @@ export function inspectCapabilities(): CapabilitiesContract {
109
118
  "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
119
  format:
111
120
  "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' };",
121
+ notes: [
122
+ "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.",
123
+ "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.",
124
+ "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.",
125
+ ],
112
126
  },
113
127
  {
114
128
  name: "references",
@@ -186,7 +200,7 @@ export function inspectCapabilities(): CapabilitiesContract {
186
200
  "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
201
  ],
188
202
  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.",
203
+ "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
204
  ],
191
205
  },
192
206
  sharedModels: {
@@ -200,7 +214,7 @@ export function inspectCapabilities(): CapabilitiesContract {
200
214
  "String formats (email/uuid/url), refinements, and transforms are not re-implemented natively; the BFF Zod schemas remain the authoritative validation.",
201
215
  ],
202
216
  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).",
217
+ "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
218
  },
205
219
  seeding: {
206
220
  createTemplates: "swallowkit create-dev-seeds <environment> — writes dev-seeds/<environment>/<model>.json templates.",
@@ -208,20 +222,29 @@ export function inspectCapabilities(): CapabilitiesContract {
208
222
  notes: ["Editing dev-seeds/*.json alone does nothing; seeds are only applied via dev --seed-env."],
209
223
  },
210
224
  machineCommands: [
211
- "inspect project | entities | routes | artifacts | boundaries | infra | drift | capabilities",
225
+ "inspect project | entities | routes | artifacts | boundaries | infra | drift | capabilities; inspect provision --plan <runId> [--wait-seconds <0-3600>]",
212
226
  "validate project",
213
227
  "generate model <names...> | generate scaffold <model>",
214
- "plan scaffold <models...> [--api-only] [--skip-infra] | plan auth --provider <p> [--scheme <name>] [--allowed-providers <csv>] | plan provision -g <rg> --location <loc> --swa-location <loc> [--adopt-existing --baseline 0 --what-if] [--reconcile --what-if]",
215
- "apply scaffold [models...] --plan <id> [--approve] [--skip-infra] | apply auth --plan <id> [--approve] | apply provision --plan <id> --approve",
228
+ "plan scaffold <models...> [--api-only] [--skip-infra] | plan auth --provider <p> [--scheme <name>] [--allowed-providers <csv>] | plan provision -g <rg> --location <loc> --swa-location <loc> [--adopt-existing --baseline 0 --what-if] [--reconcile --what-if] [--resume-deployment <name>]",
229
+ "apply scaffold [models...] --plan <id> [--approve] [--skip-infra] | apply auth --plan <id> [--approve] | apply provision --plan <id> --approve [--wait-seconds <0-3600>] (also resumes the same execution checkpoint)",
216
230
  "verify project [--checks <ids>] [--compact]",
217
231
  "explain failure [--check <id>]",
218
232
  "(regular CLI) migrations add-model <model> [--dry-run] — generate only the container migration after scaffold --skip-infra",
219
233
  "(regular CLI) configure-mcp [--version auto|latest|<semver>] — re-select the MCP launcher version in .mcp.json",
220
234
  ],
235
+ provisioning: {
236
+ execution: "Submit Bicep once with az deployment group create --no-wait, then bounded state polling (default 300 seconds, configurable 0-3600). Azure CLI invocations are bounded separately. Warning-only stderr with exit 0 is not a failure; output capture limits do not terminate az.",
237
+ completion: "status complete/outcome succeeded requires Azure Succeeded plus verified migration tags and local ledger completion. Running/Accepted -> in-progress; unreadable/not-found state -> blocked/outcome unknown; bookkeeping interruption after Azure Succeeded -> blocked/outcome partial; Failed/Canceled -> failed with Azure causes.",
238
+ tracking: "Approved runs persist before Azure writes under .swallowkit/state/provision/. runId equals the original planId and preserves the subscription, RG, deployment ID/correlation, fingerprints and migration state. inspect provision is read-only; apply provision --plan resumes without re-submitting attempted deployments. Ctrl+C/termination may leave Azure running; never cancel/delete without explicit consent.",
239
+ recovery: "For an older untracked baseline, plan provision --resume-deployment <name> compares the existing ARM template and non-secure parameters to the local Bicep/parameters. It observes that deployment and only tags/records it after Succeeded. Mismatches, unreadable evidence, secure parameters or non-baseline migrations block; do not use reconcile just to repair bookkeeping.",
240
+ diagnostics: "Results include bounded/redacted stdout/stderr, omitted-output flags, exit code, signal, failure kind, elapsed time and deployment identity. Deployment output values are not returned. Never print --debug output, keys, tokens or connection strings.",
241
+ approval: "Initial apply and every resume require explicit human approval. A passing local command or an ok envelope is not proof of completed Azure provisioning; inspect status, outcome and nextActions.",
242
+ },
221
243
  verifyChecks: {
222
- defaultChecks: ["structure", "drift", "typecheck"],
244
+ defaultChecks: ["structure", "drift", "typecheck", "backend-build"],
223
245
  optInChecks: ["build", "lint", "test"],
224
246
  customChecks: "swallowkit.config verify.checks: [{ id, command, title?, timeoutMs? }]",
247
+ 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
248
  },
226
249
  };
227
250
  }
@@ -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
@@ -57,6 +57,76 @@ export function deletePlanState(planId: string, projectRoot: string = process.cw
57
57
  }
58
58
  }
59
59
 
60
+ function provisionRunPath(runId: string, projectRoot: string): string {
61
+ if (!/^[a-f0-9]{12}$/.test(runId)) throw new Error("Invalid provision run ID.");
62
+ return path.join(projectRoot, SWALLOWKIT_STATE_DIR, "provision", `${runId}.json`);
63
+ }
64
+
65
+ export function saveProvisionRun<T extends { runId: string }>(run: T, projectRoot: string = process.cwd()): void {
66
+ ensureStateDir(projectRoot);
67
+ const file = provisionRunPath(run.runId, projectRoot);
68
+ fs.mkdirSync(path.dirname(file), { recursive: true });
69
+ const temporary = `${file}.tmp-${process.pid}`;
70
+ fs.writeFileSync(temporary, `${JSON.stringify(run, null, 2)}\n`, { encoding: "utf-8", mode: 0o600 });
71
+ fs.renameSync(temporary, file);
72
+ }
73
+
74
+ export function loadProvisionRun<T>(runId: string, projectRoot: string = process.cwd()): T | null {
75
+ const file = provisionRunPath(runId, projectRoot);
76
+ if (!fs.existsSync(file)) return null;
77
+ // Unlike a preview, discarding a corrupt execution checkpoint could replay Azure writes.
78
+ return JSON.parse(fs.readFileSync(file, "utf-8")) as T;
79
+ }
80
+
81
+ export function listProvisionRuns<T>(projectRoot: string = process.cwd()): T[] {
82
+ const directory = path.join(projectRoot, SWALLOWKIT_STATE_DIR, "provision");
83
+ if (!fs.existsSync(directory)) return [];
84
+ return fs.readdirSync(directory).filter((name) => /^[a-f0-9]{12}\.json$/.test(name))
85
+ .map((name) => loadProvisionRun<T>(name.slice(0, -5), projectRoot)!);
86
+ }
87
+
88
+ /** A live local executor must not share an Azure target with another executor. */
89
+ export function lockProvisionTarget(key: string, projectRoot: string = process.cwd()): () => void {
90
+ if (!/^[a-f0-9]{64}$/.test(key)) throw new Error("Invalid provision lock key.");
91
+ const directory = path.join(ensureStateDir(projectRoot), "provision");
92
+ fs.mkdirSync(directory, { recursive: true });
93
+ const file = path.join(directory, `${key}.lock`);
94
+ const create = () => {
95
+ const descriptor = fs.openSync(file, "wx", 0o600);
96
+ try { fs.writeFileSync(descriptor, String(process.pid)); } finally { fs.closeSync(descriptor); }
97
+ };
98
+ try {
99
+ create();
100
+ } catch (error) {
101
+ if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
102
+ const recoveryFile = `${file}.recovery`;
103
+ let descriptor: number;
104
+ try {
105
+ descriptor = fs.openSync(recoveryFile, "wx", 0o600);
106
+ } catch (recoveryError) {
107
+ if ((recoveryError as NodeJS.ErrnoException).code !== "EEXIST") throw recoveryError;
108
+ throw new Error("Provision lock recovery is already in progress. Inspect the checkpoint before retrying.");
109
+ }
110
+ fs.closeSync(descriptor);
111
+ try {
112
+ const pid = Number(fs.readFileSync(file, "utf-8"));
113
+ if (!Number.isInteger(pid) || pid < 1) throw new Error("Invalid provision lock; inspect the checkpoint before removing the lock.");
114
+ try {
115
+ process.kill(pid, 0);
116
+ } catch (probeError) {
117
+ if ((probeError as NodeJS.ErrnoException).code !== "ESRCH") throw probeError;
118
+ fs.unlinkSync(file);
119
+ create();
120
+ return () => fs.unlinkSync(file);
121
+ }
122
+ throw new Error("Another local provision executor is using this Azure target. Inspect its run; do not start a second deployment.");
123
+ } finally {
124
+ fs.unlinkSync(recoveryFile);
125
+ }
126
+ }
127
+ return () => fs.unlinkSync(file);
128
+ }
129
+
60
130
  const LAST_VERIFY_FILE = "last-verify.json";
61
131
 
62
132
  export function saveLastVerifyState(result: unknown, projectRoot: string = process.cwd()): void {
@@ -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
@@ -205,6 +214,9 @@ Goal: bootstrap Azure once, then apply only pending versioned infrastructure mig
205
214
  - Inspect infra locally first — no \`az\` calls are needed for inspection.
206
215
  - Never edit an applied migration. Create a corrective migration instead.
207
216
  - Never use \`--reconcile\` for an ordinary model or resource addition.
217
+ - A local az error/timeout or Ctrl+C does not prove Azure failed. Never
218
+ re-submit, cancel, or delete a deployment to repair a wait or bookkeeping failure.
219
+ - Respect \`status\`, \`outcome\`, and \`nextActions\`, not just \`ok\`.
208
220
 
209
221
  ## Steps
210
222
 
@@ -212,7 +224,7 @@ Goal: bootstrap Azure once, then apply only pending versioned infrastructure mig
212
224
  \`${runCmd} swallowkit machine inspect infra\`
213
225
  - review parameters, modules, outputs, and container wiring warnings.
214
226
  2. Compute the provisioning plan (no resources are created):
215
- \`${runCmd} swallowkit machine plan provision -g <resource-group> --location <region>\`
227
+ \`${runCmd} swallowkit machine plan provision -g <resource-group> --location <region> --swa-location <swa-region>\`
216
228
  - the plan reports \`bootstrap\`, \`migrate\`, \`noop\`, or \`adoption-required\`.
217
229
  - it lists only pending migrations and the exact \`az\` commands that would run.
218
230
  - for an older untagged environment, first run \`${runCmd} swallowkit migrations init --legacy-baseline\`, then plan with \`--adopt-existing --baseline 0 --what-if\`.
@@ -220,12 +232,35 @@ Goal: bootstrap Azure once, then apply only pending versioned infrastructure mig
220
232
  3. Present the plan to the human and wait for explicit approval.
221
233
  4. Only after approval:
222
234
  \`${runCmd} swallowkit machine apply provision --plan <planId> --approve\`
223
- 5. Verify the deployment outputs and report them.
235
+ 5. If \`in-progress\`, \`unknown\`, or \`partial\`, retain the execution
236
+ checkpoint and read/wait for the original deployment:
237
+ \`${runCmd} swallowkit machine inspect provision --plan <runId> --wait-seconds 300\`.
238
+ Inspection never writes Azure tags or the local ledger.
239
+ 6. After fresh explicit human approval, resume:
240
+ \`${runCmd} swallowkit machine apply provision --plan <runId> --approve\`.
241
+ Attempted deployments are not re-submitted; remaining reviewed steps and
242
+ tags/ledger finish only after Azure \`Succeeded\`.
243
+ 7. Report completion only when \`status: "complete"\` and
244
+ \`outcome: "succeeded"\`. Never report deployment output secrets.
245
+
246
+ For an older untracked bootstrap, plan with \`--resume-deployment <name>\`
247
+ and the original RG, subscription and regions. Recovery compares its ARM
248
+ template and non-secure parameters to local Bicep before approval. Mismatch,
249
+ unreadable evidence or secure parameters block; do not work around this by
250
+ using reconcile/adoption or replacing the migration manifest.
224
251
 
225
252
  ## Terminal states
226
253
 
227
254
  \`apply provision\` without \`--approve\` fails with \`approval-required\` —
228
255
  this is the guardrail working as intended, not an error to work around.
256
+
257
+ Azure \`Running\`/\`Accepted\` means \`in-progress\`. An unreadable or missing
258
+ deployment means \`unknown\`, not Azure failure. Azure \`Failed\`/\`Canceled\`
259
+ returns sanitized Azure and operation causes. Azure \`Succeeded\` with incomplete
260
+ tag/ledger writes means \`partial\`; resume bookkeeping without redeploying.
261
+ The default wait is 300 seconds (0-3600 via \`--wait-seconds\`), not unlimited.
262
+ Warning-only stderr with exit 0 is accepted. Diagnostics report local exit,
263
+ signal, elapsed time and truncated/redacted output separately from Azure state.
229
264
  `,
230
265
  };
231
266
 
@@ -254,7 +289,8 @@ under \`.github/skills/\` so skills-aware agents load them automatically.
254
289
 
255
290
  - **ai-authored** (write freely): \`shared/models/\`, business logic, custom pages/components, tests.
256
291
  - **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\`).
292
+ - **shared**: \`shared/index.ts\` allows manual exports (no markers; SwallowKit appends missing entries).
293
+ For Python \`functions/function_app.py\`, edit outside the registration marker.
258
294
  - **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
295
 
260
296
  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");