swallowkit 1.0.0-beta.57 → 1.0.0-beta.59

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 (73) hide show
  1. package/README.ja.md +1 -0
  2. package/README.md +1 -0
  3. package/dist/cli/commands/add-auth.d.ts +3 -1
  4. package/dist/cli/commands/add-auth.d.ts.map +1 -1
  5. package/dist/cli/commands/add-auth.js +55 -21
  6. package/dist/cli/commands/add-auth.js.map +1 -1
  7. package/dist/cli/commands/init.d.ts.map +1 -1
  8. package/dist/cli/commands/init.js +12 -498
  9. package/dist/cli/commands/init.js.map +1 -1
  10. package/dist/cli/index.d.ts.map +1 -1
  11. package/dist/cli/index.js +2 -0
  12. package/dist/cli/index.js.map +1 -1
  13. package/dist/core/operations/auth-operations.d.ts +4 -0
  14. package/dist/core/operations/auth-operations.d.ts.map +1 -1
  15. package/dist/core/operations/auth-operations.js +5 -2
  16. package/dist/core/operations/auth-operations.js.map +1 -1
  17. package/dist/core/operations/create-model.d.ts.map +1 -1
  18. package/dist/core/operations/create-model.js +6 -2
  19. package/dist/core/operations/create-model.js.map +1 -1
  20. package/dist/core/project/agent-instructions.d.ts +29 -0
  21. package/dist/core/project/agent-instructions.d.ts.map +1 -0
  22. package/dist/core/project/agent-instructions.js +159 -0
  23. package/dist/core/project/agent-instructions.js.map +1 -0
  24. package/dist/core/project/capabilities.d.ts +58 -0
  25. package/dist/core/project/capabilities.d.ts.map +1 -0
  26. package/dist/core/project/capabilities.js +135 -0
  27. package/dist/core/project/capabilities.js.map +1 -0
  28. package/dist/core/project/validation.d.ts.map +1 -1
  29. package/dist/core/project/validation.js +48 -0
  30. package/dist/core/project/validation.js.map +1 -1
  31. package/dist/core/project/workflows.d.ts.map +1 -1
  32. package/dist/core/project/workflows.js +46 -2
  33. package/dist/core/project/workflows.js.map +1 -1
  34. package/dist/core/scaffold/auth-generator.d.ts.map +1 -1
  35. package/dist/core/scaffold/auth-generator.js +6 -2
  36. package/dist/core/scaffold/auth-generator.js.map +1 -1
  37. package/dist/core/verify/index.d.ts +7 -0
  38. package/dist/core/verify/index.d.ts.map +1 -1
  39. package/dist/core/verify/index.js +69 -0
  40. package/dist/core/verify/index.js.map +1 -1
  41. package/dist/machine/index.d.ts.map +1 -1
  42. package/dist/machine/index.js +117 -41
  43. package/dist/machine/index.js.map +1 -1
  44. package/dist/mcp/index.d.ts.map +1 -1
  45. package/dist/mcp/index.js +32 -10
  46. package/dist/mcp/index.js.map +1 -1
  47. package/package.json +1 -1
  48. package/src/__tests__/agent-instructions.test.ts +237 -0
  49. package/src/__tests__/agent-loop-phase34.test.ts +66 -2
  50. package/src/__tests__/auth.test.ts +9 -0
  51. package/src/__tests__/init.test.ts +2 -0
  52. package/src/__tests__/machine.test.ts +15 -0
  53. package/src/__tests__/multi-auth.test.ts +12 -0
  54. package/src/__tests__/plan-apply.test.ts +38 -0
  55. package/src/cli/commands/add-auth.ts +59 -21
  56. package/src/cli/commands/init.ts +19 -525
  57. package/src/cli/index.ts +2 -0
  58. package/src/core/operations/auth-operations.ts +9 -2
  59. package/src/core/operations/create-model.ts +7 -2
  60. package/src/core/project/agent-instructions.ts +167 -0
  61. package/src/core/project/agent-templates/AGENTS.md +502 -0
  62. package/src/core/project/agent-templates/CLAUDE.md +31 -0
  63. package/src/core/project/agent-templates/azure-functions.instructions.md +22 -0
  64. package/src/core/project/agent-templates/bff-routes.instructions.md +18 -0
  65. package/src/core/project/agent-templates/copilot-instructions.md +27 -0
  66. package/src/core/project/agent-templates/shared-models.instructions.md +72 -0
  67. package/src/core/project/capabilities.ts +193 -0
  68. package/src/core/project/validation.ts +50 -0
  69. package/src/core/project/workflows.ts +48 -2
  70. package/src/core/scaffold/auth-generator.ts +7 -2
  71. package/src/core/verify/index.ts +76 -0
  72. package/src/machine/index.ts +133 -45
  73. package/src/mcp/index.ts +28 -10
@@ -0,0 +1,31 @@
1
+ # CLAUDE.md
2
+
3
+ This is a **SwallowKit** project.
4
+
5
+ Before discovery, requirements clarification, specification, planning, ticket
6
+ decomposition, implementation, verification, or review, read and follow the
7
+ root `AGENTS.md`.
8
+
9
+ SwallowKit must be considered before implementation, not only when generating
10
+ code.
11
+
12
+ Always-on rules:
13
+
14
+ - Inspect the current project and SwallowKit capabilities before making
15
+ SwallowKit-dependent assumptions or asking questions the repository can answer.
16
+ - Prefer `swallowkit_*` MCP tools.
17
+ - Fall back to `{{runCmd}} swallowkit machine ...` when MCP is unavailable.
18
+ - Inspect responsibility boundaries before editing framework-related files.
19
+ - Never hand-edit deterministic SwallowKit-managed artifacts.
20
+ - Add auth schemes only via plan/apply auth; never hand-write `auth.schemes`
21
+ in swallowkit.config (policies and `swa.allowedProviders` are hand-edited
22
+ after apply).
23
+ - Use plan/apply for deterministic generation.
24
+ - Respect all `requires-human` states and provisioning approval gates.
25
+ - Run SwallowKit verification before considering implementation complete.
26
+
27
+ Task-specific SwallowKit Agent Skills are available under `.github/skills/`.
28
+ Equivalent agent-agnostic runbooks are available under
29
+ `.swallowkit/workflows/`.
30
+
31
+ The root `AGENTS.md` is the canonical project contract.
@@ -0,0 +1,22 @@
1
+ ---
2
+ applyTo: "functions/**"
3
+ ---
4
+
5
+ # Azure Functions — Backend Rules
6
+
7
+ The root `AGENTS.md` is the canonical contract. Azure Functions own application
8
+ business logic and data access, but generated handlers and contracts may be
9
+ deterministic. Inspect boundaries before choosing an edit location.
10
+
11
+ - Put custom behavior only in an ai-authored location reported by the current
12
+ boundary contract.
13
+ - Never directly edit deterministic handlers or generated schema assets.
14
+ - Keep backend contracts aligned with `shared/models/` through MCP-first
15
+ plan/apply, using the machine interface as fallback.
16
+ - Auto-generate `id`, `createdAt`, and `updatedAt` on the backend. Never trust
17
+ client-sent values for these fields.
18
+ - Container names use PascalCase plural form and default to `/id` as the
19
+ partition key unless the source-of-truth model says otherwise.
20
+ - Respect `requires-human` and run SwallowKit verification after changes.
21
+
22
+ Backend language: {{backendLanguageLabel}}.
@@ -0,0 +1,18 @@
1
+ ---
2
+ applyTo: "app/api/**"
3
+ ---
4
+
5
+ # BFF API Routes — Rules
6
+
7
+ The root `AGENTS.md` is the canonical contract. Inspect the current
8
+ responsibility boundary before editing files in this layer.
9
+
10
+ - `app/api/` is a BFF/proxy layer. Never place business logic, database access,
11
+ or direct Cosmos DB calls here.
12
+ - Use `callFunction()` from `@/lib/api/call-function` to forward requests to
13
+ Azure Functions.
14
+ - Input/output validation may use schemas imported from `@{{projectName}}/shared`.
15
+ - Never directly edit a route reported as deterministic. Change its source of
16
+ truth and use MCP-first plan/apply, with the machine interface as fallback.
17
+ - Edit shared artifacts only outside managed markers.
18
+ - Respect `requires-human` and run SwallowKit verification after changes.
@@ -0,0 +1,27 @@
1
+ # Copilot Instructions
2
+
3
+ This is a **SwallowKit** project.
4
+
5
+ Read and follow the root `AGENTS.md` as the canonical project contract.
6
+
7
+ SwallowKit applies during discovery, requirements clarification, specification,
8
+ planning, ticket decomposition, implementation, verification, and review — not
9
+ only during code generation.
10
+
11
+ Key rules:
12
+
13
+ - Inspect current SwallowKit capabilities instead of assuming them.
14
+ - Answer repository- and SwallowKit-inspectable questions before asking the human.
15
+ - Prefer `swallowkit_*` MCP tools when available.
16
+ - Otherwise use `{{runCmd}} swallowkit machine ...`.
17
+ - Inspect responsibility boundaries before editing framework-related files.
18
+ - Never hand-edit deterministic SwallowKit-managed artifacts.
19
+ - Add auth schemes only via plan/apply auth; never hand-write `auth.schemes`
20
+ in swallowkit.config (policies and `swa.allowedProviders` are hand-edited
21
+ after apply).
22
+ - Use plan/apply for deterministic generation.
23
+ - Respect `requires-human` states and other human approval gates.
24
+ - Run SwallowKit verification before completion.
25
+
26
+ Task-specific SwallowKit Agent Skills live under `.github/skills/`.
27
+ Agent-agnostic equivalents live under `.swallowkit/workflows/`.
@@ -0,0 +1,72 @@
1
+ ---
2
+ applyTo: "shared/models/**"
3
+ ---
4
+
5
+ # Shared Models — Zod Schema Rules
6
+
7
+ The root `AGENTS.md` is the canonical contract. Files here are ai-authored
8
+ source-of-truth models, subject to the current result of
9
+ `{{runCmd}} swallowkit machine inspect boundaries`.
10
+
11
+ ## Rules
12
+
13
+ - Define Zod schemas using `zod/v4` (`import { z } from 'zod/v4'`).
14
+ - Use the Zod pattern where the schema constant and inferred TypeScript type
15
+ share the same name.
16
+ - `id` is a **required** string field: `id: z.string()`. Generated CRUD, UI,
17
+ and repositories assume `id` is always present — do NOT make it optional.
18
+ - `createdAt` and `updatedAt` are **optional** string fields
19
+ (`z.string().optional()`). The backend manages their values.
20
+ - Export a `displayName` string for UI display.
21
+ - Re-export every model from `shared/index.ts`, outside managed markers where
22
+ applicable.
23
+ - For relationships, use nested schemas rather than duplicating model shapes.
24
+
25
+ ## Optional model declarations (exact formats)
26
+
27
+ These are the only declarations SwallowKit parses from a model file:
28
+
29
+ ```ts
30
+ // Cosmos DB partition key (default '/id'). Must reference a schema field.
31
+ export const partitionKey = '/learnerId';
32
+
33
+ // Role-based access control on generated CRUD.
34
+ export const authPolicy = { roles: ['admin'] };
35
+ // or: { read: ['user'], write: ['admin'] } or: { policy: 'adminOnly' }
36
+
37
+ // Human-readable name for generated UI.
38
+ export const displayName = 'Learner';
39
+
40
+ // Bind the model to a configured external connector instead of Cosmos DB.
41
+ export const connectorConfig = {
42
+ connector: 'mysql',
43
+ table: 'learners',
44
+ operations: ['list', 'get', 'create', 'update', 'delete'],
45
+ };
46
+ ```
47
+
48
+ The full machine-readable contract:
49
+ `{{runCmd}} swallowkit machine inspect capabilities`
50
+
51
+ After changing a model, do not manually patch generated artifacts.
52
+
53
+ ## After changing a model
54
+
55
+ - The shared package is consumed via its built `dist/`. `verify project`
56
+ rebuilds it automatically, but if you typecheck manually, build the shared
57
+ package first — "no exported member" errors usually mean a stale `dist/`.
58
+ - If `dev-seeds/` exists, update the seed JSON. Seeds are only applied by
59
+ running `{{runCmd}} swallowkit dev --seed-env <environment>`; editing the
60
+ JSON alone does nothing.
61
+
62
+ Use the `swallowkit-modify-model` Agent Skill when available. Otherwise follow
63
+ `.swallowkit/workflows/modify-model.md`.
64
+
65
+ The normal agent workflow is:
66
+
67
+ ```text
68
+ inspect drift → plan scaffold → apply scaffold → verify project
69
+ ```
70
+
71
+ Prefer the equivalent `swallowkit_*` MCP tools; use the machine interface as the
72
+ fallback. Stop for any `requires-human` result.
@@ -0,0 +1,193 @@
1
+ /**
2
+ * Capabilities — 「SwallowKit に何ができるか」の機械可読な自己記述。
3
+ *
4
+ * inspect project/boundaries/infra が「今の状態」を返すのに対し、
5
+ * inspect capabilities は「モデルで宣言できるもの・認証の正しい導入手順・
6
+ * 生成 CRUD の守備範囲・シード適用方法」など、状態に依存しない
7
+ * フレームワーク仕様を返す。Coding Agent がドキュメント外の知識を
8
+ * 推測で埋めるのを構造的に防ぐことが目的。
9
+ */
10
+
11
+ import { getSwallowKitVersion } from "../../version";
12
+
13
+ export interface CapabilityModelDeclaration {
14
+ name: string;
15
+ required: boolean;
16
+ description: string;
17
+ format: string;
18
+ notes?: string[];
19
+ }
20
+
21
+ export interface CapabilityAuthProvider {
22
+ provider: string;
23
+ description: string;
24
+ postApplySteps: string[];
25
+ }
26
+
27
+ export interface CapabilitiesContract {
28
+ contractVersion: number;
29
+ swallowkitVersion: string;
30
+ modelDeclarations: CapabilityModelDeclaration[];
31
+ authentication: {
32
+ correctWorkflow: string[];
33
+ rules: string[];
34
+ providers: CapabilityAuthProvider[];
35
+ authorizationPolicies: {
36
+ semantics: string;
37
+ configExample: string;
38
+ handEditAllowed: boolean;
39
+ };
40
+ };
41
+ generatedCrud: {
42
+ guarantees: string[];
43
+ notGuaranteed: string[];
44
+ guidance: string[];
45
+ };
46
+ sharedModels: {
47
+ idFieldRule: string;
48
+ buildNote: string;
49
+ };
50
+ seeding: {
51
+ createTemplates: string;
52
+ applySeeds: string;
53
+ notes: string[];
54
+ };
55
+ machineCommands: string[];
56
+ verifyChecks: {
57
+ defaultChecks: string[];
58
+ optInChecks: string[];
59
+ customChecks: string;
60
+ };
61
+ }
62
+
63
+ export function inspectCapabilities(): CapabilitiesContract {
64
+ return {
65
+ contractVersion: 1,
66
+ swallowkitVersion: getSwallowKitVersion(),
67
+ modelDeclarations: [
68
+ {
69
+ name: "schema",
70
+ required: true,
71
+ description:
72
+ "Zod object schema; the schema constant and the inferred type share the same PascalCase name. `id` MUST be a required string — generated CRUD, UI, and repositories assume it is always present. Only `createdAt` / `updatedAt` are optional (backend-managed).",
73
+ format:
74
+ "export const Todo = z.object({ id: z.string(), title: z.string().min(1), createdAt: z.string().optional(), updatedAt: z.string().optional() });\nexport type Todo = z.infer<typeof Todo>;",
75
+ },
76
+ {
77
+ name: "displayName",
78
+ required: false,
79
+ description: "Human-readable name used by generated UI. Defaults to the model name.",
80
+ format: "export const displayName = 'Todo';",
81
+ },
82
+ {
83
+ name: "partitionKey",
84
+ required: false,
85
+ description:
86
+ "Cosmos DB partition key path for the generated container. Must start with '/' and reference a field that exists in the schema. Defaults to '/id'.",
87
+ format: "export const partitionKey = '/learnerId';",
88
+ notes: ["The referenced field (e.g. learnerId) must be declared in the Zod schema."],
89
+ },
90
+ {
91
+ name: "authPolicy",
92
+ required: false,
93
+ description:
94
+ "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.",
95
+ format:
96
+ "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' };",
97
+ },
98
+ {
99
+ name: "connectorConfig",
100
+ required: false,
101
+ description:
102
+ "Binds the model to a configured external connector (RDB / API) instead of Cosmos DB. The connector must exist in swallowkit.config connectors.",
103
+ format:
104
+ "export const connectorConfig = {\n connector: 'mysql',\n table: 'todos',\n operations: ['list', 'get', 'create', 'update', 'delete'],\n};",
105
+ },
106
+ ],
107
+ authentication: {
108
+ correctWorkflow: [
109
+ "1. plan auth --provider <custom-jwt|swa|external-token> [--scheme <name>] [--allowed-providers <csv>] — computes the change plan; SwallowKit writes auth.schemes into swallowkit.config for you.",
110
+ "2. apply auth --plan <planId> — generates auth code and appends the scheme to swallowkit.config.",
111
+ "3. Hand-edit swallowkit.config auth.authorization.policies to define named policies (the config is an extension point; this edit is expected).",
112
+ "4. For swa schemes: set swa.allowedProviders via --allowed-providers at plan time, or hand-edit it in the config after apply.",
113
+ "5. For external-token schemes: implement the generated verifier stub — it fails closed (throws) until implemented.",
114
+ "6. Re-run plan/apply scaffold for models that need auth guards, then verify project.",
115
+ ],
116
+ rules: [
117
+ "NEVER hand-write auth.schemes entries in swallowkit.config — plan/apply auth owns them and duplicate names abort with 'already exists'.",
118
+ "authorization.policies and swa.allowedProviders ARE hand-edited after apply (extension-point edits).",
119
+ "Verifier stubs pass typecheck and verify; a passing verify does NOT mean external-token auth works end-to-end.",
120
+ ],
121
+ providers: [
122
+ {
123
+ provider: "custom-jwt",
124
+ description: "Login/logout/me endpoints backed by an RDB user table, bcrypt password check, HS256 JWT.",
125
+ postApplySteps: ["Set JWT_SECRET in functions/local.settings.json.", "Ensure the user table matches auth.customJwt config."],
126
+ },
127
+ {
128
+ provider: "swa",
129
+ description: "Azure Static Web Apps built-in auth (EasyAuth). Principal arrives via x-ms-client-principal.",
130
+ postApplySteps: [
131
+ "Set swa.allowedProviders (e.g. ['github']) via --allowed-providers or by hand-editing the config; generated login URLs use the first entry.",
132
+ ],
133
+ },
134
+ {
135
+ provider: "external-token",
136
+ description: "Bearer tokens issued by an external IdP (LINE, Auth0, ...). SwallowKit generates a fail-closed verifier stub.",
137
+ postApplySteps: [
138
+ "Implement the generated verifier (extension point / generated-once; edit freely).",
139
+ "Implement the frontend token adapter (lib/auth/**/token-adapter.ts).",
140
+ ],
141
+ },
142
+ {
143
+ provider: "none",
144
+ description: "Disables authentication in the config.",
145
+ postApplySteps: [],
146
+ },
147
+ ],
148
+ authorizationPolicies: {
149
+ semantics:
150
+ "A named policy lists which schemes may satisfy it and which roles are required. Generated guards call requireAuth(request, policyName). Exactly one scheme must match the presented credentials.",
151
+ configExample:
152
+ "auth: {\n schemes: { admin: { provider: 'swa', swa: { allowedProviders: ['github'] } }, api: { provider: 'external-token' } },\n authorization: {\n defaultPolicy: 'anonymous',\n policies: {\n adminOnly: { schemes: ['admin'], roles: ['administrator'] },\n learner: { schemes: ['api'] },\n },\n },\n}",
153
+ handEditAllowed: true,
154
+ },
155
+ },
156
+ generatedCrud: {
157
+ guarantees: [
158
+ "Generated CRUD endpoints enforce the model's authPolicy (authentication + role checks).",
159
+ "Zod validation runs before persistence.",
160
+ ],
161
+ notGuaranteed: [
162
+ "Owner scoping: generated CRUD does NOT restrict rows to the authenticated principal. Any authenticated caller passing the policy can read/write any document.",
163
+ ],
164
+ guidance: [
165
+ "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.",
166
+ ],
167
+ },
168
+ sharedModels: {
169
+ idFieldRule: "id: z.string() (required). createdAt/updatedAt: z.string().optional(). The backend owns their values.",
170
+ buildNote:
171
+ "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).",
172
+ },
173
+ seeding: {
174
+ createTemplates: "swallowkit create-dev-seeds <environment> — writes dev-seeds/<environment>/<model>.json templates.",
175
+ applySeeds: "swallowkit dev --seed-env <environment> — applies the seed JSON to the local Cosmos DB emulator at dev startup.",
176
+ notes: ["Editing dev-seeds/*.json alone does nothing; seeds are only applied via dev --seed-env."],
177
+ },
178
+ machineCommands: [
179
+ "inspect project | entities | routes | artifacts | boundaries | infra | drift | capabilities",
180
+ "validate project",
181
+ "generate model <names...> | generate scaffold <model>",
182
+ "plan scaffold <models...> | plan auth --provider <p> [--scheme <name>] [--allowed-providers <csv>] | plan provision -g <rg> --location <loc> --swa-location <loc>",
183
+ "apply scaffold [models...] --plan <id> [--approve] | apply auth --plan <id> [--approve] | apply provision --plan <id> --approve",
184
+ "verify project [--checks <ids>] [--compact]",
185
+ "explain failure [--check <id>]",
186
+ ],
187
+ verifyChecks: {
188
+ defaultChecks: ["structure", "drift", "typecheck"],
189
+ optInChecks: ["build", "lint", "test"],
190
+ customChecks: "swallowkit.config verify.checks: [{ id, command, title?, timeoutMs? }]",
191
+ },
192
+ };
193
+ }
@@ -192,6 +192,55 @@ function walkFiles(directory: string, extensions: string[]): string[] {
192
192
  return collected;
193
193
  }
194
194
 
195
+ const VERIFIER_STUB_MARKER = /External token verifier(?: for \S+)? is not configured/;
196
+
197
+ /**
198
+ * external-token スキームの verifier が生成スタブのまま (fail-closed throw のみ) だと
199
+ * typecheck / verify は通るのに実行時は必ず 401/503 になる。
200
+ * 「認証は生成されたが動かない」状態を warning として可視化する。
201
+ */
202
+ function validateExternalTokenVerifierStubs(projectRoot: string, manifest: ProjectManifest, violations: ProjectViolation[]): void {
203
+ const auth = manifest.auth;
204
+ const hasExternalToken =
205
+ auth?.provider === "external-token" ||
206
+ Object.values(auth?.schemes ?? {}).some((scheme) => scheme.provider === "external-token");
207
+ if (!hasExternalToken) return;
208
+
209
+ const candidates: string[] = [
210
+ path.join("functions", "src", "auth", "external-token-verifier.ts"),
211
+ path.join("functions", "Auth", "ExternalTokenVerifier.cs"),
212
+ path.join("functions", "auth", "external_token_verifier.py"),
213
+ ];
214
+ const schemeDirs = [
215
+ { root: path.join("functions", "src", "auth", "schemes"), file: "verifier.ts" },
216
+ { root: path.join("functions", "Auth", "Schemes"), file: "ExternalTokenVerifier.cs" },
217
+ { root: path.join("functions", "auth", "schemes"), file: "verifier.py" },
218
+ ];
219
+ for (const { root, file } of schemeDirs) {
220
+ const absoluteRoot = path.join(projectRoot, root);
221
+ if (!fs.existsSync(absoluteRoot)) continue;
222
+ for (const entry of fs.readdirSync(absoluteRoot, { withFileTypes: true })) {
223
+ if (entry.isDirectory()) candidates.push(path.join(root, entry.name, file));
224
+ }
225
+ }
226
+
227
+ for (const relativePath of candidates) {
228
+ const absolutePath = path.join(projectRoot, relativePath);
229
+ if (!fs.existsSync(absolutePath)) continue;
230
+ const content = fs.readFileSync(absolutePath, "utf-8");
231
+ if (VERIFIER_STUB_MARKER.test(content)) {
232
+ pushViolation(violations, {
233
+ code: "auth-verifier-stub",
234
+ severity: "warning",
235
+ message:
236
+ "External token verifier is still the generated stub; it fails closed, so external-token authentication cannot succeed at runtime.",
237
+ location: { path: relativePath.replace(/\\/g, "/") },
238
+ suggestedFix: "Implement the verifier against your identity provider. The file is yours to edit (generated-once).",
239
+ });
240
+ }
241
+ }
242
+ }
243
+
195
244
  export async function validateProject(projectRoot: string = process.cwd()): Promise<ProjectValidationResult> {
196
245
  const loaded = await loadProjectManifest(projectRoot);
197
246
  const rebuilt = await buildProjectManifest(projectRoot);
@@ -231,6 +280,7 @@ export async function validateProject(projectRoot: string = process.cwd()): Prom
231
280
 
232
281
  validateGeneratedArtifacts(loaded.manifest, violations);
233
282
  validateLayeringViolations(projectRoot, violations);
283
+ validateExternalTokenVerifierStubs(projectRoot, loaded.manifest, violations);
234
284
 
235
285
  if (loaded.source === "file") {
236
286
  validateManifestDrift(projectRoot, loaded.manifest, rebuilt.manifest, violations);
@@ -60,7 +60,9 @@ Goal: add a data model and derive CRUD/BFF/UI/infra deterministically.
60
60
  5. Verify:
61
61
  \`${runCmd} swallowkit machine verify project\`
62
62
  - \`summary.done: false\` → \`${runCmd} swallowkit machine explain failure\` and repair, then re-verify.
63
- 6. If \`dev-seeds/\` exists, update seed JSON for the new model.
63
+ 6. If \`dev-seeds/\` exists, update seed JSON for the new model. Seeds are only
64
+ applied by \`${runCmd} swallowkit dev --seed-env <environment>\` — editing the
65
+ JSON alone does nothing.
64
66
 
65
67
  ## Terminal states
66
68
 
@@ -103,6 +105,49 @@ and regenerate. Hand-edits are detected as drift and block unattended overwrites
103
105
  `,
104
106
  };
105
107
 
108
+ const addAuth: WorkflowDoc = {
109
+ fileName: "add-auth.md",
110
+ title: "Add authentication (plan/apply with config ownership rules)",
111
+ skillName: "swallowkit-add-auth",
112
+ skillDescription:
113
+ "Add authentication (custom-jwt, Azure Static Web Apps built-in, or external-token) to a SwallowKit project via plan/apply auth, including the config ownership rules (never hand-write auth.schemes; hand-edit policies and allowedProviders after apply) and post-apply steps. Use when asked to add login, authentication, authorization, roles, or a named auth scheme.",
114
+ content: `# Workflow: Add authentication
115
+
116
+ Goal: introduce an auth scheme deterministically and finish the manual follow-ups.
117
+
118
+ ## Config ownership (critical)
119
+
120
+ - \`auth.schemes\` is **owned by plan/apply auth. Never hand-write scheme entries** —
121
+ a pre-written scheme makes planning fail with \`already exists\`.
122
+ - \`auth.authorization.policies\` and \`swa.allowedProviders\` are **hand-edited after
123
+ apply** (the config is an extension point; this is the expected workflow).
124
+
125
+ ## Steps
126
+
127
+ 1. Check what already exists: \`${runCmd} swallowkit machine inspect capabilities\`
128
+ (authentication section) and the current \`auth\` block in swallowkit.config.
129
+ 2. Plan (writes nothing):
130
+ \`${runCmd} swallowkit machine plan auth --provider <custom-jwt|swa|external-token> [--scheme <name>] [--allowed-providers github,aad]\`
131
+ 3. Apply:
132
+ \`${runCmd} swallowkit machine apply auth --plan <planId>\`
133
+ 4. Follow the returned \`nextActions\`:
134
+ - Define \`auth.authorization.policies\` referencing the scheme (hand-edit).
135
+ - swa: confirm \`swa.allowedProviders\` — generated login URLs use the first entry.
136
+ - external-token: implement the generated verifier stub. It fails closed and
137
+ **verify passes with the stub**, so a green verify does not mean the auth
138
+ flow works (a \`auth-verifier-stub\` warning is surfaced while unmodified).
139
+ 5. Add \`authPolicy\` to models that need guards, then re-run plan/apply scaffold
140
+ for them.
141
+ 6. Verify: \`${runCmd} swallowkit machine verify project\`
142
+
143
+ ## Scope guardrail
144
+
145
+ Generated CRUD enforces authentication and roles but does **not** scope rows to
146
+ the caller. Owner-scoped behavior belongs in ai-authored endpoints that derive
147
+ the scoping key from the verified principal, never from the request body.
148
+ `,
149
+ };
150
+
106
151
  const verifyRepair: WorkflowDoc = {
107
152
  fileName: "verify-and-repair.md",
108
153
  title: "Verify and repair loop",
@@ -190,6 +235,7 @@ under \`.github/skills/\` so skills-aware agents load them automatically.
190
235
  |----------|------|-------|
191
236
  | Add a new model | [add-model.md](./add-model.md) | \`swallowkit-add-model\` |
192
237
  | Modify an existing model | [modify-model.md](./modify-model.md) | \`swallowkit-modify-model\` |
238
+ | Add authentication | [add-auth.md](./add-auth.md) | \`swallowkit-add-auth\` |
193
239
  | Verify and repair | [verify-and-repair.md](./verify-and-repair.md) | \`swallowkit-verify-repair\` |
194
240
  | Provision Azure resources | [provision.md](./provision.md) | \`swallowkit-provision\` |
195
241
 
@@ -203,7 +249,7 @@ Full machine-readable contract: \`${runCmd} swallowkit machine inspect boundarie
203
249
  `,
204
250
  };
205
251
 
206
- return [index, addModel, modifyModel, verifyRepair, provision];
252
+ return [index, addModel, modifyModel, addAuth, verifyRepair, provision];
207
253
  }
208
254
 
209
255
  export const WORKFLOWS_DIR = path.join(".swallowkit", "workflows");
@@ -61,6 +61,11 @@ function safeIdentifier(name: string): string {
61
61
  return name.replace(/[^A-Za-z0-9_]/g, "_");
62
62
  }
63
63
 
64
+ /** C# の string[] リテラル。空のときは型推論不能な `new[] { }` (CS0826) を避けて Array.Empty<string>() を出力する。 */
65
+ function csharpStringArray(items: string[]): string {
66
+ return items.length > 0 ? `new[] { ${items.map((item) => `"${item}"`).join(", ")} }` : "Array.Empty<string>()";
67
+ }
68
+
64
69
  function schemeSlug(name: string): string {
65
70
  return name.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/_/g, "-").toLowerCase();
66
71
  }
@@ -123,10 +128,10 @@ public static class AuthRouter
123
128
  {
124
129
  private static readonly Dictionary<string, (string[] Schemes, string[] Roles)> Policies = new()
125
130
  {
126
- ${Object.entries(config.authorization.policies).map(([n,p]) => ` ["${n}"] = (new[] { ${p.schemes.map(s=>`"${s}"`).join(", ")} }, new[] { ${(p.roles??[]).map(r=>`"${r}"`).join(", ")} }),`).join("\n")}
131
+ ${Object.entries(config.authorization.policies).map(([n,p]) => ` ["${n}"] = (${csharpStringArray(p.schemes)}, ${csharpStringArray(p.roles??[])}),`).join("\n")}
127
132
  };
128
133
  private static readonly Dictionary<string,string> Providers = new() { ${Object.entries(config.schemes).map(([n,s])=>`["${n}"]="${s.provider}"`).join(", ")} };
129
- private static readonly Dictionary<string,string[]> AllowedProviders = new() { ${Object.entries(config.schemes).map(([n,s])=>`["${n}"]=new[] { ${(s.swa?.allowedProviders??[]).map(p=>`"${p}"`).join(", ")} }`).join(", ")} };
134
+ private static readonly Dictionary<string,string[]> AllowedProviders = new() { ${Object.entries(config.schemes).map(([n,s])=>`["${n}"]=${csharpStringArray(s.swa?.allowedProviders??[])}`).join(", ")} };
130
135
  public static async Task<(AuthPrincipal? Principal, HttpResponseData? ErrorResponse)> Authorize(HttpRequestData request, string policyName)
131
136
  {
132
137
  if (!Policies.TryGetValue(policyName, out var policy)) return (null, await Error(request, HttpStatusCode.Unauthorized, "Undefined authorization policy"));
@@ -29,6 +29,8 @@ export interface VerifyCheckEvidence {
29
29
  violations?: ProjectViolation[];
30
30
  findings?: DriftFinding[];
31
31
  logTail?: string[];
32
+ /** --compact で抑制された info-severity findings の件数。 */
33
+ suppressedInfoFindings?: number;
32
34
  }
33
35
 
34
36
  export interface VerifyCheckResult {
@@ -213,6 +215,23 @@ function runTypecheckCheck(projectRoot: string): VerifyCheckResult {
213
215
  };
214
216
  }
215
217
 
218
+ // typecheck は shared のビルド済み dist/ を参照する。古い dist による
219
+ // 「no exported member」誤検知を防ぐため、先に shared をビルドする。
220
+ const sharedBuild = runSharedBuildIfPresent(projectRoot);
221
+ if (sharedBuild.ran && !sharedBuild.ok) {
222
+ return {
223
+ id: "typecheck",
224
+ title: "TypeScript type check",
225
+ status: "fail",
226
+ durationMs: Date.now() - startedAt,
227
+ 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
+ ],
232
+ };
233
+ }
234
+
216
235
  const scripts = readPackageScripts(projectRoot);
217
236
  let command: string;
218
237
  let args: string[];
@@ -240,6 +259,42 @@ const SCRIPT_CHECK_TITLES: Record<string, string> = {
240
259
  test: "Test suite",
241
260
  };
242
261
 
262
+ interface SharedBuildOutcome {
263
+ ran: boolean;
264
+ ok: boolean;
265
+ command?: string;
266
+ logTail?: string[];
267
+ }
268
+
269
+ /** shared/package.json に build script があれば実行する (なければ no-op)。 */
270
+ function runSharedBuildIfPresent(projectRoot: string): SharedBuildOutcome {
271
+ const sharedPackageJsonPath = path.join(projectRoot, "shared", "package.json");
272
+ if (!fs.existsSync(sharedPackageJsonPath)) return { ran: false, ok: true };
273
+ try {
274
+ const pkg = JSON.parse(fs.readFileSync(sharedPackageJsonPath, "utf-8")) as { scripts?: Record<string, string> };
275
+ if (typeof pkg.scripts?.build !== "string") return { ran: false, ok: true };
276
+ } catch {
277
+ return { ran: false, ok: true };
278
+ }
279
+
280
+ const pm = detectFromProject(projectRoot);
281
+ const command = getCommands(pm).name;
282
+ const result = spawnSync(command, ["run", "build"], {
283
+ cwd: path.join(projectRoot, "shared"),
284
+ shell: true,
285
+ encoding: "utf-8",
286
+ timeout: 5 * 60 * 1000,
287
+ });
288
+ const output = `${result.stdout ?? ""}\n${result.stderr ?? ""}`;
289
+ const ok = result.status === 0 && !result.error;
290
+ return {
291
+ ran: true,
292
+ ok,
293
+ command: `${command} run build (in shared/)`,
294
+ logTail: ok ? undefined : tailLines(result.error ? `${output}\n${result.error.message}` : output, 50),
295
+ };
296
+ }
297
+
243
298
  function runScriptCheck(projectRoot: string, scriptName: "build" | "lint" | "test"): VerifyCheckResult {
244
299
  const startedAt = Date.now();
245
300
  const scripts = readPackageScripts(projectRoot);
@@ -358,6 +413,27 @@ export async function runVerify(
358
413
  return result;
359
414
  }
360
415
 
416
+ /**
417
+ * Agent 消費向けに info-severity の drift findings (「user-owned が編集されたが想定内」等)
418
+ * を除去したコンパクトな結果を返す。判定 (summary) は変わらない。
419
+ */
420
+ export function compactVerifyResult(result: VerifyResult): VerifyResult {
421
+ return {
422
+ ...result,
423
+ checks: result.checks.map((check) => {
424
+ const findings = check.evidence.findings;
425
+ if (!findings || findings.length === 0) return check;
426
+ const kept = findings.filter((finding) => finding.severity !== "info");
427
+ const suppressed = findings.length - kept.length;
428
+ if (suppressed === 0) return check;
429
+ return {
430
+ ...check,
431
+ evidence: { ...check.evidence, findings: kept, suppressedInfoFindings: suppressed },
432
+ };
433
+ }),
434
+ };
435
+ }
436
+
361
437
  export interface FailureExplanation {
362
438
  checkId: string;
363
439
  title: string;