vibepro 0.1.0-alpha.0

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 (89) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +9 -0
  3. package/README.ja.md +448 -0
  4. package/README.md +520 -0
  5. package/agent-instructions/codex/AGENTS.vibepro.md +45 -0
  6. package/bin/vibepro.js +9 -0
  7. package/docs/assets/vibepro-header.png +0 -0
  8. package/package.json +51 -0
  9. package/skills/vibepro-diagnosis-packages/SKILL.md +133 -0
  10. package/skills/vibepro-human-review/SKILL.md +73 -0
  11. package/skills/vibepro-story-refactor/SKILL.md +89 -0
  12. package/skills/vibepro-workflow/SKILL.md +139 -0
  13. package/src/agent-harness-map.js +230 -0
  14. package/src/agent-harness-scanner.js +337 -0
  15. package/src/agent-review.js +2180 -0
  16. package/src/api-boundary-scanner.js +452 -0
  17. package/src/architecture-profiler.js +423 -0
  18. package/src/authorization-scoring.js +149 -0
  19. package/src/brainbase-importer.js +534 -0
  20. package/src/change-risk-classifier.js +195 -0
  21. package/src/check-packs.js +605 -0
  22. package/src/checkpoint-manager.js +233 -0
  23. package/src/cli.js +2213 -0
  24. package/src/code-quality-scanner.js +310 -0
  25. package/src/codex-manager.js +143 -0
  26. package/src/component-style-scanner.js +336 -0
  27. package/src/coverage-report.js +99 -0
  28. package/src/database-access-scanner.js +163 -0
  29. package/src/decision-records.js +315 -0
  30. package/src/design-modernize.js +1435 -0
  31. package/src/design-system.js +1732 -0
  32. package/src/diagnostic-engine.js +1945 -0
  33. package/src/diagram-requirement-resolver.js +194 -0
  34. package/src/doctor.js +677 -0
  35. package/src/environment-graph.js +424 -0
  36. package/src/execution-state.js +849 -0
  37. package/src/explore-evidence.js +425 -0
  38. package/src/flow-design-scanner.js +896 -0
  39. package/src/flow-verifier.js +887 -0
  40. package/src/gesture-interaction-scanner.js +330 -0
  41. package/src/graph-context.js +263 -0
  42. package/src/graphify-adapter.js +189 -0
  43. package/src/html-report.js +1035 -0
  44. package/src/journey-map.js +1299 -0
  45. package/src/language.js +48 -0
  46. package/src/lazy-pattern-detector.js +182 -0
  47. package/src/local-dev-scanner.js +135 -0
  48. package/src/managed-worktree-gate.js +187 -0
  49. package/src/managed-worktree.js +766 -0
  50. package/src/merge-manager.js +501 -0
  51. package/src/network-contract-scanner.js +442 -0
  52. package/src/nocodb-story-sync.js +386 -0
  53. package/src/oss-readiness-scanner.js +417 -0
  54. package/src/performance-evidence.js +756 -0
  55. package/src/performance-measurer.js +591 -0
  56. package/src/pr-manager.js +8220 -0
  57. package/src/presets.js +682 -0
  58. package/src/public-discovery-scanner.js +519 -0
  59. package/src/refactoring-delta-reporter.js +367 -0
  60. package/src/refactoring-opportunity-generator.js +797 -0
  61. package/src/regression-risk-scanner.js +146 -0
  62. package/src/repo-status.js +266 -0
  63. package/src/report-fingerprint.js +188 -0
  64. package/src/report-pr-body-prompt-template.md +108 -0
  65. package/src/report-pr-body-schema.json +95 -0
  66. package/src/report-store.js +135 -0
  67. package/src/report-validator.js +192 -0
  68. package/src/requirement-consistency.js +1066 -0
  69. package/src/runtime-info.js +134 -0
  70. package/src/self-dogfood-scanner.js +476 -0
  71. package/src/session-learning.js +164 -0
  72. package/src/skills-manager.js +157 -0
  73. package/src/spec-drift.js +378 -0
  74. package/src/spec-fingerprint.js +445 -0
  75. package/src/spec-prompt-template.md +155 -0
  76. package/src/spec-schema.json +219 -0
  77. package/src/spec-store.js +258 -0
  78. package/src/spec-validator.js +459 -0
  79. package/src/static-site-scanner.js +316 -0
  80. package/src/story-candidate-generator.js +85 -0
  81. package/src/story-catalog-generator.js +2813 -0
  82. package/src/story-html.js +156 -0
  83. package/src/story-manager.js +2144 -0
  84. package/src/story-task-generator.js +522 -0
  85. package/src/task-manager.js +1029 -0
  86. package/src/terminal-link-scanner.js +238 -0
  87. package/src/usage-report.js +417 -0
  88. package/src/verification-evidence.js +284 -0
  89. package/src/workspace.js +126 -0
@@ -0,0 +1,423 @@
1
+ import { readdir, readFile, stat } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ const PROFILER_IGNORED_DIRS = new Set([
5
+ '.git',
6
+ '.next',
7
+ '.turbo',
8
+ '.vibepro',
9
+ '.worktrees',
10
+ 'coverage',
11
+ 'node_modules',
12
+ 'graphify-out'
13
+ ]);
14
+ const PROFILER_PACKAGE_MANAGERS = [
15
+ { file: 'pnpm-lock.yaml', name: 'pnpm' },
16
+ { file: 'yarn.lock', name: 'yarn' },
17
+ { file: 'package-lock.json', name: 'npm' }
18
+ ];
19
+ const PROFILER_FRAMEWORK_LANGUAGES = new Map([
20
+ ['.ts', 'typescript'],
21
+ ['.tsx', 'typescript'],
22
+ ['.js', 'javascript'],
23
+ ['.jsx', 'javascript'],
24
+ ['.mjs', 'javascript'],
25
+ ['.py', 'python'],
26
+ ['.rb', 'ruby'],
27
+ ['.go', 'go'],
28
+ ['.rs', 'rust'],
29
+ ['.php', 'php']
30
+ ]);
31
+
32
+ export async function profileArchitecture(repoRoot) {
33
+ const root = path.resolve(repoRoot);
34
+ const files = await collectFiles(root);
35
+ const fileSet = new Set(files.map((file) => file.relativePath));
36
+ const packageJson = await readPackageJson(root, fileSet);
37
+ const dependencies = collectDependencies(packageJson);
38
+ const auth = detectAuth({ dependencies, fileSet });
39
+ const appType = detectAppType({ fileSet, dependencies });
40
+ const rendering = detectRendering({ dependencies });
41
+ const frameworks = detectFrameworks(dependencies);
42
+ const languages = detectLanguages(files);
43
+ const apiRoutes = listApiRoutes(fileSet);
44
+ const database = detectDatabase(dependencies);
45
+ const deployment = detectDeployment(fileSet);
46
+ const views = buildArchitectureViews({
47
+ appType,
48
+ rendering,
49
+ frameworks,
50
+ languages,
51
+ apiRoutes,
52
+ database,
53
+ auth,
54
+ deployment,
55
+ dependencies,
56
+ fileSet
57
+ });
58
+ const profile = {
59
+ system_type: toSystemType(appType),
60
+ app_type: appType,
61
+ rendering,
62
+ frameworks,
63
+ package_manager: detectPackageManager(fileSet),
64
+ languages,
65
+ views,
66
+ has_api_routes: views.runtime.entrypoints.length > 0,
67
+ has_database: views.data.stores.length > 0 || views.data.access_patterns.length > 0,
68
+ database,
69
+ has_auth: auth.length > 0,
70
+ auth,
71
+ deployment,
72
+ evidence: buildProfileEvidence({ fileSet, packageJson, dependencies })
73
+ };
74
+ const checkCatalog = selectCheckCatalog(profile);
75
+
76
+ return {
77
+ ...profile,
78
+ applicable_checks: checkCatalog.applicable_checks,
79
+ selected_views: checkCatalog.selected_views
80
+ };
81
+ }
82
+
83
+ function detectAppType({ fileSet, dependencies }) {
84
+ if (dependencies.has('next') || dependencies.has('react') || dependencies.has('vue') || dependencies.has('svelte')) {
85
+ return 'web_app';
86
+ }
87
+ if (fileSet.has('index.html')) return 'static_site';
88
+ return 'unknown';
89
+ }
90
+
91
+ function detectRendering({ dependencies }) {
92
+ if (dependencies.has('next')) return 'nextjs';
93
+ if (dependencies.has('react')) return 'react';
94
+ if (dependencies.has('vue')) return 'vue';
95
+ if (dependencies.has('svelte')) return 'svelte';
96
+ return null;
97
+ }
98
+
99
+ function detectFrameworks(dependencies) {
100
+ return ['next', 'react', 'vue', 'svelte']
101
+ .filter((dependency) => dependencies.has(dependency))
102
+ .map((dependency) => dependency === 'next' ? 'nextjs' : dependency);
103
+ }
104
+
105
+ function toSystemType(appType) {
106
+ if (appType === 'web_app') return 'web_application';
107
+ if (appType === 'static_site') return 'static_site';
108
+ return 'unknown';
109
+ }
110
+
111
+ function detectPackageManager(fileSet) {
112
+ const manager = PROFILER_PACKAGE_MANAGERS.find((candidate) => fileSet.has(candidate.file));
113
+ if (manager) return manager.name;
114
+ return fileSet.has('package.json') ? 'npm' : null;
115
+ }
116
+
117
+ function detectLanguages(files) {
118
+ return [...new Set(files
119
+ .map((file) => PROFILER_FRAMEWORK_LANGUAGES.get(path.extname(file.relativePath).toLowerCase()))
120
+ .filter(Boolean))]
121
+ .sort();
122
+ }
123
+
124
+ function hasApiRoutes(fileSet) {
125
+ return listApiRoutes(fileSet).length > 0;
126
+ }
127
+
128
+ function listApiRoutes(fileSet) {
129
+ return [...fileSet].filter((file) => (
130
+ /^app\/api\/.+\/route\.(js|jsx|ts|tsx)$/.test(file)
131
+ || /^src\/app\/api\/.+\/route\.(js|jsx|ts|tsx)$/.test(file)
132
+ || /^pages\/api\/.+\.(js|jsx|ts|tsx)$/.test(file)
133
+ || /^src\/pages\/api\/.+\.(js|jsx|ts|tsx)$/.test(file)
134
+ )).sort();
135
+ }
136
+
137
+ function detectDatabase(dependencies) {
138
+ const database = [];
139
+ if (dependencies.has('@supabase/supabase-js')) database.push('supabase');
140
+ if (dependencies.has('@prisma/client') || dependencies.has('prisma')) database.push('prisma');
141
+ if (dependencies.has('pg')) database.push('postgres');
142
+ if (dependencies.has('drizzle-orm')) database.push('drizzle');
143
+ if (dependencies.has('kysely')) database.push('kysely');
144
+ if (dependencies.has('mongoose')) database.push('mongodb');
145
+ if (dependencies.has('sequelize')) database.push('sequelize');
146
+ return database;
147
+ }
148
+
149
+ function detectAuth({ dependencies, fileSet }) {
150
+ const auth = [];
151
+ if (dependencies.has('next-auth')) auth.push('next-auth');
152
+ if (dependencies.has('@auth/core')) auth.push('authjs');
153
+ if (dependencies.has('@clerk/nextjs')) auth.push('clerk');
154
+ if (dependencies.has('@supabase/supabase-js')) auth.push('supabase-auth');
155
+ if (dependencies.has('passport')) auth.push('passport');
156
+ if (hasNextMiddleware(fileSet)) auth.push('next-middleware');
157
+ if (hasNextAuthRoute(fileSet)) auth.push('next-auth-route');
158
+ return auth;
159
+ }
160
+
161
+ function hasNextMiddleware(fileSet) {
162
+ return fileSet.has('middleware.ts')
163
+ || fileSet.has('middleware.js')
164
+ || fileSet.has('src/middleware.ts')
165
+ || fileSet.has('src/middleware.js');
166
+ }
167
+
168
+ function hasNextAuthRoute(fileSet) {
169
+ return [...fileSet].some((file) => (
170
+ /^app\/api\/auth\/.+\.(js|jsx|ts|tsx)$/.test(file)
171
+ || /^src\/app\/api\/auth\/.+\.(js|jsx|ts|tsx)$/.test(file)
172
+ || /^pages\/api\/auth\/.+\.(js|jsx|ts|tsx)$/.test(file)
173
+ || /^src\/pages\/api\/auth\/.+\.(js|jsx|ts|tsx)$/.test(file)
174
+ ));
175
+ }
176
+
177
+ function detectDeployment(fileSet) {
178
+ const deployment = [];
179
+ if (fileSet.has('vercel.json')) deployment.push('vercel');
180
+ if (fileSet.has('fly.toml')) deployment.push('fly');
181
+ if (fileSet.has('wrangler.toml')) deployment.push('cloudflare');
182
+ if (fileSet.has('Dockerfile') || fileSet.has('docker-compose.yml') || fileSet.has('docker-compose.yaml')) {
183
+ deployment.push('docker');
184
+ }
185
+ return deployment;
186
+ }
187
+
188
+ function buildArchitectureViews({
189
+ appType,
190
+ rendering,
191
+ frameworks,
192
+ languages,
193
+ apiRoutes,
194
+ database,
195
+ auth,
196
+ deployment,
197
+ dependencies,
198
+ fileSet
199
+ }) {
200
+ const structureComponents = [];
201
+ if (apiRoutes.length > 0) structureComponents.push('api_routes');
202
+ if (hasNextMiddleware(fileSet)) structureComponents.push('middleware');
203
+ if (hasPages(fileSet)) structureComponents.push('pages');
204
+ if (fileSet.has('index.html')) structureComponents.push('static_entry');
205
+
206
+ return {
207
+ structure: {
208
+ containers: appType === 'unknown' ? [] : [appType],
209
+ components: structureComponents,
210
+ frameworks,
211
+ languages
212
+ },
213
+ runtime: {
214
+ entrypoints: [...apiRoutes, ...listMiddlewareFiles(fileSet)],
215
+ server_boundaries: [
216
+ ...(apiRoutes.length > 0 ? ['api_routes'] : []),
217
+ ...(hasServerActions(fileSet) ? ['server_actions'] : []),
218
+ ...(hasNextMiddleware(fileSet) ? ['middleware'] : [])
219
+ ],
220
+ rendering
221
+ },
222
+ data: {
223
+ stores: database.filter((item) => ['postgres', 'mongodb', 'supabase'].includes(item)),
224
+ access_patterns: database.filter((item) => !['postgres', 'mongodb', 'supabase'].includes(item))
225
+ },
226
+ security: {
227
+ auth_boundaries: buildAuthBoundaries(fileSet),
228
+ auth_mechanisms: auth,
229
+ secret_files: listEnvFiles(fileSet)
230
+ },
231
+ deployment: {
232
+ targets: deployment,
233
+ config_files: listDeploymentFiles(fileSet)
234
+ },
235
+ quality: {
236
+ test_tools: detectTestTools(dependencies),
237
+ ci: listCiFiles(fileSet)
238
+ }
239
+ };
240
+ }
241
+
242
+ function selectCheckCatalog(profile) {
243
+ const checks = ['secrets', 'xss', 'dependency-graph'];
244
+ const selectedViews = ['structure'];
245
+ if (profile.app_type === 'web_app' || profile.app_type === 'static_site') {
246
+ checks.push('component-style');
247
+ }
248
+ if (profile.languages.some((language) => ['javascript', 'typescript'].includes(language))) {
249
+ selectedViews.push('quality');
250
+ checks.push('code-quality');
251
+ }
252
+ if (profile.views.runtime.entrypoints.length > 0 || profile.views.runtime.server_boundaries.length > 0) {
253
+ selectedViews.push('runtime');
254
+ checks.push('api-boundary');
255
+ }
256
+ if (profile.app_type === 'static_site') {
257
+ checks.push('static-entry', 'static-publish-surface', 'external-resources');
258
+ }
259
+ if (profile.views.data.stores.length > 0 || profile.views.data.access_patterns.length > 0) {
260
+ selectedViews.push('data');
261
+ checks.push('database-access');
262
+ }
263
+ if (profile.views.security.auth_boundaries.length > 0 || profile.views.security.auth_mechanisms.length > 0) {
264
+ selectedViews.push('security');
265
+ checks.push('auth-boundary');
266
+ }
267
+ if (profile.views.deployment.targets.length > 0) {
268
+ selectedViews.push('deployment');
269
+ checks.push('deployment-readiness');
270
+ }
271
+ if (profile.views.quality.test_tools.length > 0 || profile.views.quality.ci.length > 0) {
272
+ selectedViews.push('quality');
273
+ }
274
+ return {
275
+ selected_views: [...new Set(selectedViews)],
276
+ applicable_checks: [...new Set(checks)]
277
+ };
278
+ }
279
+
280
+ function buildProfileEvidence({ fileSet, packageJson, dependencies }) {
281
+ const evidence = [];
282
+ if (fileSet.has('package.json')) {
283
+ evidence.push({ kind: 'package_json', file: 'package.json', detail: packageJson?.name ?? 'package.json' });
284
+ }
285
+ if (dependencies.has('next')) {
286
+ evidence.push({ kind: 'framework', file: 'package.json', detail: 'next' });
287
+ }
288
+ if (hasApiRoutes(fileSet)) {
289
+ evidence.push({ kind: 'api_routes', file: findFirstApiRoute(fileSet), detail: 'API route detected' });
290
+ }
291
+ if (hasNextMiddleware(fileSet)) {
292
+ evidence.push({ kind: 'auth_boundary', file: findNextMiddleware(fileSet), detail: 'Next.js middleware detected' });
293
+ }
294
+ if (hasNextAuthRoute(fileSet)) {
295
+ evidence.push({ kind: 'auth_boundary', file: findFirstNextAuthRoute(fileSet), detail: 'Auth route detected' });
296
+ }
297
+ for (const deploymentFile of ['vercel.json', 'fly.toml', 'wrangler.toml', 'Dockerfile']) {
298
+ if (fileSet.has(deploymentFile)) {
299
+ evidence.push({ kind: 'deployment', file: deploymentFile, detail: deploymentFile });
300
+ }
301
+ }
302
+ return evidence;
303
+ }
304
+
305
+ function findFirstApiRoute(fileSet) {
306
+ return listApiRoutes(fileSet)[0] ?? null;
307
+ }
308
+
309
+ function findNextMiddleware(fileSet) {
310
+ return ['middleware.ts', 'middleware.js', 'src/middleware.ts', 'src/middleware.js']
311
+ .find((file) => fileSet.has(file)) ?? null;
312
+ }
313
+
314
+ function findFirstNextAuthRoute(fileSet) {
315
+ return [...fileSet].find((file) => (
316
+ /^app\/api\/auth\/.+\.(js|jsx|ts|tsx)$/.test(file)
317
+ || /^src\/app\/api\/auth\/.+\.(js|jsx|ts|tsx)$/.test(file)
318
+ || /^pages\/api\/auth\/.+\.(js|jsx|ts|tsx)$/.test(file)
319
+ || /^src\/pages\/api\/auth\/.+\.(js|jsx|ts|tsx)$/.test(file)
320
+ )) ?? null;
321
+ }
322
+
323
+ function buildAuthBoundaries(fileSet) {
324
+ return [
325
+ ...listMiddlewareFiles(fileSet).map((file) => ({ type: 'middleware', file })),
326
+ ...listNextAuthRoutes(fileSet).map((file) => ({ type: 'auth_route', file }))
327
+ ];
328
+ }
329
+
330
+ function listMiddlewareFiles(fileSet) {
331
+ return ['middleware.ts', 'middleware.js', 'src/middleware.ts', 'src/middleware.js']
332
+ .filter((file) => fileSet.has(file));
333
+ }
334
+
335
+ function listNextAuthRoutes(fileSet) {
336
+ return [...fileSet].filter((file) => (
337
+ /^app\/api\/auth\/.+\.(js|jsx|ts|tsx)$/.test(file)
338
+ || /^src\/app\/api\/auth\/.+\.(js|jsx|ts|tsx)$/.test(file)
339
+ || /^pages\/api\/auth\/.+\.(js|jsx|ts|tsx)$/.test(file)
340
+ || /^src\/pages\/api\/auth\/.+\.(js|jsx|ts|tsx)$/.test(file)
341
+ )).sort();
342
+ }
343
+
344
+ function listEnvFiles(fileSet) {
345
+ return [...fileSet].filter((file) => {
346
+ const basename = path.basename(file);
347
+ return basename === '.env' || basename.startsWith('.env.');
348
+ }).sort();
349
+ }
350
+
351
+ function listDeploymentFiles(fileSet) {
352
+ return ['vercel.json', 'fly.toml', 'wrangler.toml', 'Dockerfile', 'docker-compose.yml', 'docker-compose.yaml']
353
+ .filter((file) => fileSet.has(file));
354
+ }
355
+
356
+ function detectTestTools(dependencies) {
357
+ return [...new Set(['vitest', 'jest', 'playwright', '@playwright/test', 'cypress']
358
+ .filter((dependency) => dependencies.has(dependency))
359
+ .map((dependency) => dependency === '@playwright/test' ? 'playwright' : dependency))];
360
+ }
361
+
362
+ function listCiFiles(fileSet) {
363
+ return [...fileSet].filter((file) => file.startsWith('.github/workflows/') || file.startsWith('.circleci/')).sort();
364
+ }
365
+
366
+ function hasPages(fileSet) {
367
+ return [...fileSet].some((file) => (
368
+ /^app\/.+\.(js|jsx|ts|tsx)$/.test(file)
369
+ || /^src\/app\/.+\.(js|jsx|ts|tsx)$/.test(file)
370
+ || /^pages\/.+\.(js|jsx|ts|tsx)$/.test(file)
371
+ || /^src\/pages\/.+\.(js|jsx|ts|tsx)$/.test(file)
372
+ ));
373
+ }
374
+
375
+ function hasServerActions(fileSet) {
376
+ return [...fileSet].some((file) => /actions?\.(js|jsx|ts|tsx)$/.test(file));
377
+ }
378
+
379
+ function collectDependencies(packageJson) {
380
+ const dependencyGroups = [
381
+ packageJson?.dependencies,
382
+ packageJson?.devDependencies,
383
+ packageJson?.peerDependencies,
384
+ packageJson?.optionalDependencies
385
+ ];
386
+ return new Set(dependencyGroups.flatMap((group) => Object.keys(group ?? {})));
387
+ }
388
+
389
+ async function readPackageJson(root, fileSet) {
390
+ if (!fileSet.has('package.json')) return null;
391
+ try {
392
+ return JSON.parse(await readFile(path.join(root, 'package.json'), 'utf8'));
393
+ } catch {
394
+ return null;
395
+ }
396
+ }
397
+
398
+ function hasAnyDependency(dependencies, names) {
399
+ return names.some((name) => dependencies.has(name));
400
+ }
401
+
402
+ async function collectFiles(root, current = root) {
403
+ const entries = await readdir(current, { withFileTypes: true });
404
+ const files = [];
405
+
406
+ for (const entry of entries) {
407
+ if (entry.isDirectory() && PROFILER_IGNORED_DIRS.has(entry.name)) continue;
408
+ const absolutePath = path.join(current, entry.name);
409
+ const relativePath = path.relative(root, absolutePath).split(path.sep).join('/');
410
+
411
+ if (entry.isDirectory()) {
412
+ files.push(...await collectFiles(root, absolutePath));
413
+ continue;
414
+ }
415
+
416
+ if (!entry.isFile()) continue;
417
+ const fileStat = await stat(absolutePath);
418
+ if (fileStat.size > 1024 * 1024) continue;
419
+ files.push({ absolutePath, relativePath });
420
+ }
421
+
422
+ return files;
423
+ }
@@ -0,0 +1,149 @@
1
+ const SCHEMA_VERSION = '0.1.0';
2
+
3
+ const SURFACE_ALIASES = {
4
+ frontend_interaction: ['frontend', 'ui', 'interaction', 'screen', 'component', 'フロント', '画面'],
5
+ server_api: ['server_api', 'api', 'endpoint', 'route', 'サーバー'],
6
+ service_orchestration: ['service', 'orchestration', 'orchestrator', 'サービス'],
7
+ core_workflow_state: ['workflow', 'state machine', 'orchestration', 'state', 'transition', 'ワークフロー', '状態'],
8
+ gate_orchestration: ['gate', 'pr-manager', 'gate orchestration'],
9
+ verification_evidence: ['verification', 'evidence', 'flow verifier'],
10
+ review_lifecycle: ['review lifecycle', 'agent review', 'lifecycle'],
11
+ database_state: ['database', 'db', 'schema', 'persistence'],
12
+ queue_worker: ['queue', 'worker', 'job', 'background'],
13
+ polling_retry: ['polling', 'poll', 'retry', 'status check'],
14
+ auth_boundary: ['auth', 'authentication', 'authorization', 'permission', 'session', '認証'],
15
+ legacy_v1_compatibility: ['legacy', 'v1', '互換']
16
+ };
17
+
18
+ const MATRIX = {
19
+ light: { high: 'allow', medium: 'allow', low: 'allow', unknown: 'allow' },
20
+ ui_interaction: { high: 'allow', medium: 'allow', low: 'require_human_review', unknown: 'require_human_review' },
21
+ api_contract: { high: 'allow', medium: 'require_human_review', low: 'require_human_review', unknown: 'block' },
22
+ workflow_heavy: { high: 'allow', medium: 'require_human_review', low: 'block', unknown: 'block' }
23
+ };
24
+
25
+ const KNOWN_PROFILES = new Set(Object.keys(MATRIX));
26
+
27
+ export function scoreAuthorization({ riskProfile = null, storySource = null, decisions = [] } = {}) {
28
+ const profile = riskProfile?.profile ?? 'light';
29
+ const riskSurfaces = Array.isArray(riskProfile?.risk_surfaces) ? riskProfile.risk_surfaces : [];
30
+ const hasStory = isNonEmptyStory(storySource);
31
+ const hasDecisions = Array.isArray(decisions) && decisions.length > 0;
32
+
33
+ const signals = [];
34
+ const acceptedSignals = collectAcceptedDecisionSignals(decisions, riskSurfaces);
35
+ signals.push(...acceptedSignals.signals);
36
+
37
+ const storySignals = collectStorySurfaceSignals(storySource, riskSurfaces);
38
+ signals.push(...storySignals);
39
+
40
+ let authorizationLevel;
41
+ if (acceptedSignals.qualifies) {
42
+ authorizationLevel = 'high';
43
+ } else if (storySignals.some((signal) => signal.kind === 'acceptance_criteria_mentions_surface' || signal.kind === 'story_background_mentions_surface')) {
44
+ authorizationLevel = 'medium';
45
+ } else if (!hasStory && !hasDecisions) {
46
+ authorizationLevel = 'unknown';
47
+ } else if (riskSurfaces.length === 0) {
48
+ authorizationLevel = 'unknown';
49
+ } else {
50
+ authorizationLevel = 'low';
51
+ }
52
+
53
+ const matrixProfile = KNOWN_PROFILES.has(profile) ? profile : null;
54
+ const recommendation = matrixProfile
55
+ ? MATRIX[matrixProfile][authorizationLevel]
56
+ : 'require_human_review';
57
+
58
+ return {
59
+ schema_version: SCHEMA_VERSION,
60
+ authorization_level: authorizationLevel,
61
+ signals,
62
+ review_outcome_recommendation: recommendation,
63
+ matrix_cell: {
64
+ risk_profile: profile,
65
+ authorization_level: authorizationLevel,
66
+ known_profile: matrixProfile !== null
67
+ }
68
+ };
69
+ }
70
+
71
+ function isNonEmptyStory(storySource) {
72
+ if (!storySource || typeof storySource !== 'object') return false;
73
+ const fields = [
74
+ storySource.title,
75
+ storySource.requirement_title,
76
+ storySource.background,
77
+ storySource.policy,
78
+ ...(Array.isArray(storySource.acceptance_criteria) ? storySource.acceptance_criteria : [])
79
+ ];
80
+ return fields.some((value) => typeof value === 'string' && value.trim().length > 0);
81
+ }
82
+
83
+ function collectAcceptedDecisionSignals(decisions, riskSurfaces) {
84
+ const signals = [];
85
+ let qualifies = false;
86
+ if (!Array.isArray(decisions)) return { signals, qualifies };
87
+ for (const decision of decisions) {
88
+ if (!decision || typeof decision !== 'object') continue;
89
+ if (decision.status !== 'accepted') continue;
90
+ const source = typeof decision.source === 'string' ? decision.source.trim() : '';
91
+ if (!source) {
92
+ signals.push({
93
+ kind: 'decision_record_invalid_source',
94
+ decision_id: decision.decision_id ?? null,
95
+ reason: 'accepted decision has no source reference'
96
+ });
97
+ continue;
98
+ }
99
+ if (!isPlausibleSourceReference(source)) {
100
+ signals.push({
101
+ kind: 'decision_record_invalid_source',
102
+ decision_id: decision.decision_id ?? null,
103
+ source,
104
+ reason: 'source does not look like a gate or finding id'
105
+ });
106
+ continue;
107
+ }
108
+ qualifies = true;
109
+ signals.push({
110
+ kind: 'decision_record_accepted',
111
+ decision_id: decision.decision_id ?? null,
112
+ source,
113
+ addresses_risk_surface: riskSurfaces.find((surface) => source.toLowerCase().includes(surface)) ?? null
114
+ });
115
+ }
116
+ return { signals, qualifies };
117
+ }
118
+
119
+ function isPlausibleSourceReference(source) {
120
+ return /^(gate:|finding:|check:|review:|dec-|decision-)/i.test(source) || source.includes(':');
121
+ }
122
+
123
+ function collectStorySurfaceSignals(storySource, riskSurfaces) {
124
+ if (!isNonEmptyStory(storySource) || riskSurfaces.length === 0) return [];
125
+ const acceptance = Array.isArray(storySource.acceptance_criteria) ? storySource.acceptance_criteria : [];
126
+ const background = [storySource.background, storySource.policy, storySource.title, storySource.requirement_title]
127
+ .filter((value) => typeof value === 'string')
128
+ .join('\n');
129
+ const signals = [];
130
+ for (const surface of riskSurfaces) {
131
+ const aliases = SURFACE_ALIASES[surface] ?? [];
132
+ const haystack = (str) => surfaceMatches(str, surface, aliases);
133
+ if (acceptance.some(haystack)) {
134
+ signals.push({ kind: 'acceptance_criteria_mentions_surface', surface });
135
+ } else if (haystack(background)) {
136
+ signals.push({ kind: 'story_background_mentions_surface', surface });
137
+ }
138
+ }
139
+ return signals;
140
+ }
141
+
142
+ function surfaceMatches(text, surface, aliases) {
143
+ if (typeof text !== 'string' || text.length === 0) return false;
144
+ const lower = text.toLowerCase();
145
+ if (lower.includes(surface.toLowerCase())) return true;
146
+ const surfaceWords = surface.replace(/_/g, ' ').toLowerCase();
147
+ if (lower.includes(surfaceWords)) return true;
148
+ return aliases.some((alias) => lower.includes(alias.toLowerCase()));
149
+ }