@unson/brainbase-mcp 0.44.0 → 0.44.1

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 (72) hide show
  1. package/contracts/canonical-task/operation-schema.sql +58 -0
  2. package/contracts/canonical-task/search-indexes.sql +10 -0
  3. package/contracts/canonical-task/store-schema.sql +77 -0
  4. package/dist/automation-run.d.ts +213 -0
  5. package/dist/automation-run.js +479 -0
  6. package/dist/canonical-task-operations.d.ts +205 -0
  7. package/dist/canonical-task-operations.js +700 -0
  8. package/dist/canonical-task-postgres.d.ts +139 -0
  9. package/dist/canonical-task-postgres.js +554 -0
  10. package/dist/canonical-task-service.d.ts +13 -0
  11. package/dist/canonical-task-service.js +55 -3
  12. package/dist/canonical-task-store-config.d.ts +31 -0
  13. package/dist/canonical-task-store-config.js +53 -0
  14. package/dist/config-parser.d.ts +298 -0
  15. package/dist/config-parser.js +593 -0
  16. package/dist/connected-onboarding.d.ts +57 -0
  17. package/dist/connected-onboarding.js +37 -0
  18. package/dist/durable-wait-http.d.ts +3 -1
  19. package/dist/durable-wait-http.js +73 -6
  20. package/dist/durable-waits.d.ts +93 -3
  21. package/dist/durable-waits.js +320 -8
  22. package/dist/honcho.d.ts +25 -0
  23. package/dist/honcho.js +81 -0
  24. package/dist/judgment-event-adapter.d.ts +72 -0
  25. package/dist/judgment-event-adapter.js +114 -0
  26. package/dist/judgment-event-outbox.d.ts +182 -0
  27. package/dist/judgment-event-outbox.js +568 -0
  28. package/dist/judgment-execution-outcome.d.ts +46 -0
  29. package/dist/judgment-execution-outcome.js +149 -0
  30. package/dist/judgment-host.d.ts +18 -7
  31. package/dist/judgment-host.js +82 -34
  32. package/dist/judgment-lifecycle-core.d.ts +198 -0
  33. package/dist/judgment-lifecycle-core.js +464 -0
  34. package/dist/judgment-node-evidence.d.ts +57 -0
  35. package/dist/judgment-node-evidence.js +297 -0
  36. package/dist/judgment-runtime-resolution.d.ts +264 -0
  37. package/dist/judgment-runtime-resolution.js +1248 -0
  38. package/dist/judgment-runtime-resolution.public.d.ts +255 -0
  39. package/dist/knowledge-event.d.ts +28 -0
  40. package/dist/knowledge-event.js +114 -0
  41. package/dist/mcp-runtime.d.ts +75 -0
  42. package/dist/mcp-runtime.js +188 -0
  43. package/dist/meeting-automation-runtime.d.ts +102 -0
  44. package/dist/meeting-automation-runtime.js +249 -0
  45. package/dist/meeting-automation.d.ts +155 -0
  46. package/dist/meeting-automation.js +341 -0
  47. package/dist/meeting-minutes-context-receipt.d.ts +91 -0
  48. package/dist/meeting-minutes-context-receipt.js +280 -0
  49. package/dist/meeting-source-sync.d.ts +89 -0
  50. package/dist/meeting-source-sync.js +230 -0
  51. package/dist/mesh.d.ts +58 -0
  52. package/dist/mesh.js +134 -0
  53. package/dist/onboarding-runtime.d.ts +47 -0
  54. package/dist/onboarding-runtime.js +4719 -0
  55. package/dist/routine-cycle.d.ts +51 -0
  56. package/dist/routine-cycle.js +153 -0
  57. package/dist/routine-liveness.d.ts +49 -0
  58. package/dist/routine-liveness.js +227 -0
  59. package/dist/routine-runtime.d.ts +101 -0
  60. package/dist/routine-runtime.js +237 -0
  61. package/dist/routines.d.ts +3 -0
  62. package/dist/routines.js +3 -0
  63. package/dist/run-receipt.d.ts +40 -0
  64. package/dist/run-receipt.js +635 -0
  65. package/dist/runtime-host.d.ts +64 -0
  66. package/dist/runtime-host.js +70 -0
  67. package/dist/world-work.d.ts +16 -0
  68. package/package.json +115 -3
  69. package/ui/objective-editor.js +47 -11
  70. package/ui/world/world-district.js +18 -3
  71. package/ui/world/world-placement.js +13 -0
  72. package/ui/world/world-work-rail.js +7 -1
@@ -0,0 +1,593 @@
1
+ /**
2
+ * Config Parser - brainbaseの設定ファイル(YAML)を読み取り、統合ビューを提供
3
+ */
4
+ import fs from 'fs/promises';
5
+ import path from 'path';
6
+ import yaml from 'js-yaml';
7
+ import { logger as defaultLogger } from './server-support.js';
8
+ function requirePath(name, value) {
9
+ if (typeof value !== 'string' || value.length === 0) {
10
+ throw new TypeError(`ConfigParser requires an explicit ${name} path`);
11
+ }
12
+ return value;
13
+ }
14
+ export class ConfigParser {
15
+ /**
16
+ * Construct a parser with every filesystem boundary supplied by the host.
17
+ *
18
+ * The OSS package deliberately has no workspace or repository defaults. A
19
+ * host that needs compatibility defaults must provide them in its adapter.
20
+ *
21
+ * @param paths filesystem boundaries and optional catalog mode supplied by the host
22
+ */
23
+ configPath;
24
+ slackDir;
25
+ brainbaseRoot;
26
+ projectsRoot;
27
+ workspaceRoot;
28
+ catalogMode;
29
+ logger;
30
+ _configSourceStatus;
31
+ _configCache;
32
+ _configPromise;
33
+ constructor(paths) {
34
+ const { configPath, slackDir, brainbaseRoot, projectsRoot, catalogMode = 'required', logger = defaultLogger } = paths ?? {};
35
+ this.configPath = requirePath('configPath', configPath);
36
+ this.slackDir = requirePath('slackDir', slackDir);
37
+ this.brainbaseRoot = requirePath('brainbaseRoot', brainbaseRoot);
38
+ this.projectsRoot = requirePath('projectsRoot', projectsRoot);
39
+ this.catalogMode = catalogMode;
40
+ this.logger = logger;
41
+ this._configSourceStatus = this.catalogMode === 'disabled' ? 'not_applicable' : 'not_loaded';
42
+ // workspaceRoot = parent directory of brainbaseRoot (where 'general' project resides)
43
+ this.workspaceRoot = path.dirname(this.brainbaseRoot);
44
+ this._configCache = null;
45
+ this._configPromise = null;
46
+ }
47
+ /**
48
+ * @template T
49
+ * @param {string} filePath
50
+ * @param {T} defaultValue
51
+ * @param {string} [label]
52
+ * @returns {Promise<T>}
53
+ */
54
+ async _loadYamlFile(filePath, defaultValue, label) {
55
+ try {
56
+ const content = await fs.readFile(filePath, 'utf-8');
57
+ const data = yaml.load(content);
58
+ return data ?? defaultValue;
59
+ }
60
+ catch (err) {
61
+ const target = label || filePath;
62
+ const error = err;
63
+ if (error?.code === 'ENOENT' && filePath.startsWith(this.slackDir)) {
64
+ this.logger.debug(`Optional config missing: ${target}`, { filePath });
65
+ return defaultValue;
66
+ }
67
+ this.logger.error(`Failed to load ${target}:`, { error: error.message || String(error) });
68
+ return defaultValue;
69
+ }
70
+ }
71
+ /**
72
+ * @template T
73
+ * @param {string} filename
74
+ * @param {T} defaultValue
75
+ * @returns {Promise<T>}
76
+ */
77
+ async _loadSlackYaml(filename, defaultValue) {
78
+ return this._loadYamlFile(path.join(this.slackDir, filename), defaultValue, filename);
79
+ }
80
+ async _loadConfig() {
81
+ if (this._configCache) {
82
+ return this._configCache;
83
+ }
84
+ if (this.catalogMode === 'disabled') {
85
+ this._configSourceStatus = 'not_applicable';
86
+ this._configCache = {};
87
+ this.logger.debug('Project catalog is disabled for this runtime', {
88
+ mode: this.catalogMode
89
+ });
90
+ return this._configCache;
91
+ }
92
+ if (!this._configPromise) {
93
+ this._configPromise = fs.readFile(this.configPath, 'utf-8')
94
+ .then((content) => {
95
+ const data = yaml.load(content);
96
+ this._configSourceStatus = 'loaded';
97
+ return data ?? {};
98
+ })
99
+ .catch((err) => {
100
+ const error = err;
101
+ this._configSourceStatus = error?.code === 'ENOENT' ? 'missing' : 'invalid';
102
+ this.logger.error('Failed to load config.yml:', {
103
+ error: error.message || String(error),
104
+ configPath: this.configPath
105
+ });
106
+ return {};
107
+ });
108
+ }
109
+ this._configCache = await this._configPromise;
110
+ return this._configCache;
111
+ }
112
+ /**
113
+ * SPEC-settings-phase0-guards INV-4: ConfigService.write 後に呼ぶことで stale cache を回避
114
+ */
115
+ invalidateCache() {
116
+ this._configCache = null;
117
+ this._configPromise = null;
118
+ }
119
+ /**
120
+ * @param {ProjectConfig[]} [projects]
121
+ * @returns {ProjectConfig[]}
122
+ */
123
+ _expandProjectPaths(projects = []) {
124
+ return projects.map(project => {
125
+ if (project.local?.path) {
126
+ const expandedPath = project.local.path.replace(/\$\{PROJECTS_ROOT:-[^}]+\}/g, this.projectsRoot);
127
+ return {
128
+ ...project,
129
+ local: {
130
+ ...project.local,
131
+ path: expandedPath
132
+ }
133
+ };
134
+ }
135
+ return project;
136
+ });
137
+ }
138
+ /**
139
+ * Slackワークスペース一覧を取得
140
+ */
141
+ async getWorkspaces() {
142
+ const data = await this._loadSlackYaml('workspaces.yml', { workspaces: {} });
143
+ return data?.workspaces || {};
144
+ }
145
+ /**
146
+ * Slackチャンネル一覧を取得
147
+ */
148
+ async getChannels() {
149
+ const data = await this._loadSlackYaml('channels.yml', { channels: [] });
150
+ return data?.channels || [];
151
+ }
152
+ /**
153
+ * Slackメンバー一覧を取得
154
+ */
155
+ async getMembers() {
156
+ const data = await this._loadSlackYaml('members.yml', { members: [] });
157
+ return data?.members || [];
158
+ }
159
+ /**
160
+ * プロジェクト設定(config.yml)を取得
161
+ */
162
+ async getProjects() {
163
+ const data = await this._loadConfig();
164
+ const projects = this._expandProjectPaths(data.projects || []).map(project => {
165
+ // Legacyの未認証設定APIへProject Profileのtenant・人物・接続意図を露出させない。
166
+ const { capabilities: _capabilities, people: _people, organization: _organization, created_by: _createdBy, success_criteria: _successCriteria, project_code: _projectCode, ...legacyProject } = project;
167
+ return legacyProject;
168
+ });
169
+ return {
170
+ root: this.workspaceRoot, // Workspace root directory (where 'general' project resides)
171
+ projects
172
+ };
173
+ }
174
+ /**
175
+ * GitHubマッピングを取得(config.ymlから抽出)
176
+ */
177
+ async getGitHubMappings() {
178
+ const data = await this._loadConfig();
179
+ const projects = data.projects || [];
180
+ return projects
181
+ .filter(p => p.github)
182
+ .map(p => ({
183
+ project_id: p.id,
184
+ owner: p.github?.owner,
185
+ repo: p.github?.repo,
186
+ branch: p.github?.branch || 'main',
187
+ url: `https://github.com/${p.github?.owner}/${p.github?.repo}`
188
+ }));
189
+ }
190
+ /**
191
+ * Airtableマッピングを取得(config.ymlから抽出)
192
+ */
193
+ async getAirtableMappings() {
194
+ const data = await this._loadConfig();
195
+ const projects = data.projects || [];
196
+ return projects
197
+ .filter(p => p.airtable)
198
+ .map(p => ({
199
+ project_id: p.id,
200
+ base_id: p.airtable?.base_id,
201
+ base_name: p.airtable?.base_name,
202
+ url: `https://airtable.com/${p.airtable?.base_id}`
203
+ }));
204
+ }
205
+ /**
206
+ * UI Plugins設定を取得(config.ymlから抽出)
207
+ * @returns {Promise<any>} { enabled: string[] } - 有効なプラグインID一覧
208
+ */
209
+ async getPlugins() {
210
+ const data = await this._loadConfig();
211
+ const plugins = data.plugins || {};
212
+ if (!plugins.enabled && !plugins.disabled) {
213
+ // OSSの初期状態: 必要最小限のプラグインのみ有効化
214
+ return {
215
+ enabled: ['bb-inbox'],
216
+ disabled: []
217
+ };
218
+ }
219
+ return {
220
+ enabled: Array.isArray(plugins.enabled) ? plugins.enabled : [],
221
+ disabled: Array.isArray(plugins.disabled) ? plugins.disabled : []
222
+ };
223
+ }
224
+ /**
225
+ * 統合ビュー: Workspace → Project → Slack/GitHub/Airtable の関係を一覧
226
+ */
227
+ async getUnifiedView() {
228
+ const [workspaces, channels, projectConfig, githubMappings] = await Promise.all([
229
+ this.getWorkspaces(),
230
+ this.getChannels(),
231
+ this.getProjects(),
232
+ this.getGitHubMappings()
233
+ ]);
234
+ // プロジェクト情報をIDでインデックス化
235
+ const projectsById = {};
236
+ for (const p of projectConfig.projects) {
237
+ if (p.id) {
238
+ projectsById[p.id] = p;
239
+ }
240
+ }
241
+ // チャンネルをproject_idでグループ化
242
+ const channelsByProjectId = {};
243
+ for (const ch of channels) {
244
+ // proj_xxx → xxx に正規化
245
+ const normalizedId = ch.project_id?.replace(/^proj_/, '') || '';
246
+ if (!channelsByProjectId[normalizedId]) {
247
+ channelsByProjectId[normalizedId] = [];
248
+ }
249
+ channelsByProjectId[normalizedId].push({
250
+ id: ch.channel_id,
251
+ name: ch.channel_name,
252
+ type: ch.type || 'general',
253
+ workspace: ch.workspace
254
+ });
255
+ }
256
+ // GitHubマッピングをproject_idでインデックス化
257
+ const githubByProjectId = {};
258
+ for (const g of githubMappings) {
259
+ if (g.project_id) {
260
+ githubByProjectId[g.project_id] = g;
261
+ }
262
+ }
263
+ // ワークスペースごとにプロジェクトを整理
264
+ const result = {
265
+ workspaces: [],
266
+ orphanedChannels: [],
267
+ orphanedProjects: []
268
+ };
269
+ // 使用済みプロジェクトを追跡(正規化後のID)
270
+ const usedProjectIds = new Set();
271
+ // ID正規化ヘルパー: proj_zeims → zeims
272
+ const normalizeId = (id) => id.replace(/^proj_/, '');
273
+ for (const [wsKey, ws] of Object.entries(workspaces)) {
274
+ const wsProjects = [];
275
+ // ワークスペースに紐づくプロジェクトを処理
276
+ const projectIds = ws.projects || [];
277
+ for (const projId of projectIds) {
278
+ // proj_zeims → zeims に正規化してマッチング
279
+ const normalizedProjId = normalizeId(projId);
280
+ usedProjectIds.add(normalizedProjId);
281
+ const project = projectsById[normalizedProjId];
282
+ const github = githubByProjectId[normalizedProjId];
283
+ const projectChannels = channelsByProjectId[normalizedProjId] || [];
284
+ // glob_include からパスを抽出
285
+ let paths = null;
286
+ if (project?.local?.glob_include) {
287
+ paths = project.local.glob_include.map(g => {
288
+ const match = g.match(/^([^*]+)/);
289
+ return match ? match[1].replace(/\/$/, '') + '/' : null;
290
+ }).filter((p) => p !== null);
291
+ }
292
+ wsProjects.push({
293
+ id: projId,
294
+ emoji: project?.emoji || null,
295
+ archived: project?.archived || false,
296
+ channels: projectChannels.filter(ch => ch.workspace === wsKey),
297
+ github: github ? {
298
+ owner: github.owner,
299
+ repo: github.repo,
300
+ branch: github.branch,
301
+ url: github.url,
302
+ paths: paths
303
+ } : null
304
+ });
305
+ }
306
+ result.workspaces.push({
307
+ key: wsKey,
308
+ name: ws.name,
309
+ id: ws.id,
310
+ projects: wsProjects
311
+ });
312
+ }
313
+ // 孤立プロジェクト(どのワークスペースにも属していない)
314
+ for (const p of projectConfig.projects) {
315
+ const projectId = p.id;
316
+ if (!projectId || !usedProjectIds.has(projectId)) {
317
+ result.orphanedProjects.push({
318
+ id: projectId,
319
+ hasGithub: projectId ? !!githubByProjectId[projectId] : false,
320
+ channelCount: projectId ? (channelsByProjectId[projectId] || []).length : 0
321
+ });
322
+ }
323
+ }
324
+ // 孤立チャンネル(proj_otherや未マッピング)
325
+ const allMappedProjectIds = new Set([
326
+ ...Object.keys(projectsById),
327
+ ...Object.values(workspaces).flatMap(ws => ws.projects || [])
328
+ ]);
329
+ for (const ch of channels) {
330
+ const normalizedId = ch.project_id?.replace(/^proj_/, '') || '';
331
+ if (!allMappedProjectIds.has(normalizedId) && normalizedId !== 'other') {
332
+ result.orphanedChannels.push({
333
+ id: ch.channel_id,
334
+ name: ch.channel_name,
335
+ workspace: ch.workspace,
336
+ project_id: ch.project_id
337
+ });
338
+ }
339
+ }
340
+ return result;
341
+ }
342
+ /**
343
+ * Organizations(法人)設定を取得
344
+ * @returns {Promise<any[]>} 法人一覧
345
+ */
346
+ async getOrganizations() {
347
+ const data = await this._loadConfig();
348
+ return data.organizations || [];
349
+ }
350
+ /**
351
+ * Dependencies(依存関係)設定を取得
352
+ * @returns {Promise<any>} 依存関係マッピング
353
+ */
354
+ async getDependencies() {
355
+ const data = await this._loadConfig();
356
+ return data.dependencies || {};
357
+ }
358
+ /**
359
+ * Notifications(通知)設定を取得
360
+ * @returns {Promise<any>} 通知設定
361
+ */
362
+ async getNotifications() {
363
+ const data = await this._loadConfig();
364
+ return data.notifications || {
365
+ channels: {},
366
+ dnd: {}
367
+ };
368
+ }
369
+ /**
370
+ * 全設定を統合して取得
371
+ */
372
+ async getAll() {
373
+ const [workspaces, channels, members, projectConfig, githubMappings, airtableMappings, plugins] = await Promise.all([
374
+ this.getWorkspaces(),
375
+ this.getChannels(),
376
+ this.getMembers(),
377
+ this.getProjects(),
378
+ this.getGitHubMappings(),
379
+ this.getAirtableMappings(),
380
+ this.getPlugins()
381
+ ]);
382
+ return {
383
+ slack: {
384
+ workspaces,
385
+ channels,
386
+ members
387
+ },
388
+ projects: projectConfig,
389
+ github: githubMappings,
390
+ airtable: airtableMappings,
391
+ plugins
392
+ };
393
+ }
394
+ /**
395
+ * 整合性チェック
396
+ * - チャンネルに定義されているが、プロジェクトに存在しないproject_id
397
+ * - ワークスペースに定義されているが、チャンネルがないワークスペース
398
+ * - 重複したSlack ID
399
+ * - GitHubマッピングの検証
400
+ */
401
+ async checkIntegrity() {
402
+ if (this.catalogMode === 'disabled') {
403
+ return {
404
+ applicability: 'not_applicable',
405
+ source: {
406
+ status: 'not_applicable',
407
+ path: this.configPath,
408
+ mode: this.catalogMode
409
+ },
410
+ issues: [],
411
+ summary: { total: 0, errors: 0, warnings: 0, info: 0 },
412
+ stats: {
413
+ workspaces: 0,
414
+ channels: 0,
415
+ members: 0,
416
+ projects: 0,
417
+ github: 0,
418
+ airtable: 0
419
+ }
420
+ };
421
+ }
422
+ const [workspaces, channels, members, projectConfig, githubMappings, airtableMappings] = await Promise.all([
423
+ this.getWorkspaces(),
424
+ this.getChannels(),
425
+ this.getMembers(),
426
+ this.getProjects(),
427
+ this.getGitHubMappings(),
428
+ this.getAirtableMappings()
429
+ ]);
430
+ const issues = [];
431
+ if (this._configSourceStatus !== 'loaded') {
432
+ issues.push({
433
+ type: 'config_source_unavailable',
434
+ severity: 'error',
435
+ message: `Required project catalog is ${this._configSourceStatus}: ${this.configPath}`,
436
+ source: 'config.yml'
437
+ });
438
+ }
439
+ // プロジェクトIDのセットを作成
440
+ const configProjectIds = new Set(projectConfig.projects
441
+ .map(p => p.id)
442
+ .filter((id) => typeof id === 'string'));
443
+ // ワークスペースで定義されているプロジェクトIDのセット
444
+ const workspaceProjectIds = new Set();
445
+ Object.values(workspaces).forEach(ws => {
446
+ (ws.projects || []).forEach(pid => workspaceProjectIds.add(pid));
447
+ });
448
+ // チャンネルで使われているプロジェクトIDのセット
449
+ const channelProjectIds = new Set();
450
+ channels.forEach(ch => {
451
+ if (ch.project_id) {
452
+ channelProjectIds.add(ch.project_id);
453
+ }
454
+ });
455
+ // 1. チャンネルのproject_idがconfig.ymlに存在するか
456
+ // proj_otherは意図的な「その他」カテゴリなので除外
457
+ const ignoredProjectIds = new Set(['proj_other', 'other']);
458
+ channelProjectIds.forEach(pid => {
459
+ // 除外リストにあるプロジェクトはスキップ
460
+ if (ignoredProjectIds.has(pid))
461
+ return;
462
+ // proj_xxx → xxxに変換してチェック
463
+ const shortId = pid.replace(/^proj_/, '');
464
+ if (!configProjectIds.has(shortId) && !configProjectIds.has(pid)) {
465
+ issues.push({
466
+ type: 'missing_project',
467
+ severity: 'warning',
468
+ message: `Channel uses project_id "${pid}" but not found in config.yml`,
469
+ source: 'channels.yml'
470
+ });
471
+ }
472
+ });
473
+ // 2. ワークスペースごとのチャンネル数をカウント
474
+ const workspaceChannelCount = {};
475
+ channels.forEach(ch => {
476
+ if (ch.workspace) {
477
+ workspaceChannelCount[ch.workspace] = (workspaceChannelCount[ch.workspace] || 0) + 1;
478
+ }
479
+ });
480
+ Object.keys(workspaces).forEach(wsKey => {
481
+ if (!workspaceChannelCount[wsKey]) {
482
+ issues.push({
483
+ type: 'empty_workspace',
484
+ severity: 'info',
485
+ message: `Workspace "${wsKey}" has no mapped channels`,
486
+ source: 'workspaces.yml'
487
+ });
488
+ }
489
+ });
490
+ // 3. メンバーの重複チェック(同じslack_idが複数ある場合)
491
+ const slackIdCount = {};
492
+ members.forEach(m => {
493
+ if (m.slack_id) {
494
+ slackIdCount[m.slack_id] = (slackIdCount[m.slack_id] || 0) + 1;
495
+ }
496
+ });
497
+ Object.entries(slackIdCount).forEach(([slackId, count]) => {
498
+ if (count > 1) {
499
+ const memberNames = members
500
+ .filter(m => m.slack_id === slackId)
501
+ .map(m => m.brainbase_name)
502
+ .filter((name) => typeof name === 'string');
503
+ // 同一人物のサブアカウントの場合は問題なし
504
+ const uniqueNames = [...new Set(memberNames)];
505
+ if (uniqueNames.length > 1) {
506
+ issues.push({
507
+ type: 'duplicate_slack_id',
508
+ severity: 'error',
509
+ message: `Slack ID "${slackId}" is mapped to multiple people: ${uniqueNames.join(', ')}`,
510
+ source: 'members.yml'
511
+ });
512
+ }
513
+ }
514
+ });
515
+ // 4. チャンネルの重複チェック
516
+ const channelIdCount = {};
517
+ channels.forEach(ch => {
518
+ if (ch.channel_id) {
519
+ channelIdCount[ch.channel_id] = (channelIdCount[ch.channel_id] || 0) + 1;
520
+ }
521
+ });
522
+ Object.entries(channelIdCount).forEach(([channelId, count]) => {
523
+ if (count > 1) {
524
+ issues.push({
525
+ type: 'duplicate_channel_id',
526
+ severity: 'warning',
527
+ message: `Channel ID "${channelId}" appears ${count} times`,
528
+ source: 'channels.yml'
529
+ });
530
+ }
531
+ });
532
+ // 5. GitHubマッピングの検証
533
+ // 5a. ローカルパスがあるがGitHubがないプロジェクト
534
+ const projectsWithLocal = projectConfig.projects.filter(p => p.local?.path);
535
+ const githubProjectIds = new Set(githubMappings
536
+ .map(g => g.project_id)
537
+ .filter((id) => typeof id === 'string'));
538
+ projectsWithLocal.forEach(p => {
539
+ if (!p.id || !githubProjectIds.has(p.id)) {
540
+ issues.push({
541
+ type: 'missing_github',
542
+ severity: 'info',
543
+ message: `Project "${p.id}" has local path but no GitHub mapping`,
544
+ source: 'config.yml'
545
+ });
546
+ }
547
+ });
548
+ // 5b. GitHubマッピングの重複チェック
549
+ const repoCount = {};
550
+ githubMappings.forEach(g => {
551
+ const key = `${g.owner}/${g.repo}`;
552
+ if (!repoCount[key])
553
+ repoCount[key] = [];
554
+ if (g.project_id) {
555
+ repoCount[key].push(g.project_id);
556
+ }
557
+ });
558
+ Object.entries(repoCount).forEach(([repo, projectIds]) => {
559
+ if (projectIds.length > 1) {
560
+ issues.push({
561
+ type: 'shared_github_repo',
562
+ severity: 'info',
563
+ message: `GitHub repo "${repo}" is shared by projects: ${projectIds.join(', ')}`,
564
+ source: 'config.yml'
565
+ });
566
+ }
567
+ });
568
+ // 6. Airtableマッピングの検証は無効化(2026-01-17)
569
+ return {
570
+ applicability: 'applicable',
571
+ source: {
572
+ status: this._configSourceStatus,
573
+ path: this.configPath,
574
+ mode: this.catalogMode
575
+ },
576
+ issues,
577
+ summary: {
578
+ total: issues.length,
579
+ errors: issues.filter(i => i.severity === 'error').length,
580
+ warnings: issues.filter(i => i.severity === 'warning').length,
581
+ info: issues.filter(i => i.severity === 'info').length
582
+ },
583
+ stats: {
584
+ workspaces: Object.keys(workspaces).length,
585
+ channels: channels.length,
586
+ members: members.length,
587
+ projects: projectConfig.projects.length,
588
+ github: githubMappings.length,
589
+ airtable: airtableMappings.length
590
+ }
591
+ };
592
+ }
593
+ }
@@ -1,3 +1,60 @@
1
+ /**
2
+ * Candidate content scanning shared by onboarding hosts.
3
+ *
4
+ * The Org server keeps the same legacy result shape (`block`, `review`, and
5
+ * named counts), while the implementation and detector vocabulary live in the
6
+ * OSS onboarding boundary. Keep these detectors separate from the recursive
7
+ * persistent-value guard above: the former reports review findings, while the
8
+ * latter rejects unsafe ledger values before they are written.
9
+ */
10
+ export type OnboardingScanKind = 'block' | 'review';
11
+ export interface OnboardingScanFinding {
12
+ kind: OnboardingScanKind;
13
+ name: string;
14
+ count: number;
15
+ }
16
+ export interface OnboardingContentScan {
17
+ block: boolean;
18
+ review: boolean;
19
+ findings: OnboardingScanFinding[];
20
+ }
21
+ export declare const ONBOARDING_BLOCK_PATTERNS: readonly [{
22
+ readonly name: "openai_api_key";
23
+ readonly regex: RegExp;
24
+ }, {
25
+ readonly name: "github_pat";
26
+ readonly regex: RegExp;
27
+ }, {
28
+ readonly name: "anthropic_api_key";
29
+ readonly regex: RegExp;
30
+ }, {
31
+ readonly name: "aws_access_key";
32
+ readonly regex: RegExp;
33
+ }, {
34
+ readonly name: "slack_token";
35
+ readonly regex: RegExp;
36
+ }, {
37
+ readonly name: "private_key_block";
38
+ readonly regex: RegExp;
39
+ }, {
40
+ readonly name: "password_assignment";
41
+ readonly regex: RegExp;
42
+ }, {
43
+ readonly name: "infisical_ref";
44
+ readonly regex: RegExp;
45
+ }];
46
+ export declare const ONBOARDING_PII_PATTERNS: readonly [{
47
+ readonly name: "jp_phone";
48
+ readonly regex: RegExp;
49
+ }, {
50
+ readonly name: "email";
51
+ readonly regex: RegExp;
52
+ }, {
53
+ readonly name: "credit_card_like";
54
+ readonly regex: RegExp;
55
+ }];
56
+ /** Scan candidate text without exposing raw content in the finding output. */
57
+ export declare function scanOnboardingContent(content: unknown): OnboardingContentScan;
1
58
  declare const WORKFLOW_STATES: readonly ["initialized", "source_ready", "candidates_ready", "promotion_reviewed", "first_value_ready", "first_value_answer_reviewed"];
2
59
  export type SourceMode = 'mcp' | 'drive' | 'gmail' | 'local_folder' | 'single_document';
3
60
  export type SourceStatus = 'ready' | 'waiting_for_authorization' | 'unavailable' | 'error' | 'unconfirmed';
@@ -15,6 +15,43 @@ const RAW_CONTENT_KEY = /^(?:raw(?:_body|_content|_text)?|body|content|document|
15
15
  const SECRET_VALUE = /(?:^|\s)Bearer\s+[A-Za-z0-9._~+/-]+=*|\bsk-[A-Za-z0-9_-]{16,}\b/i;
16
16
  const SECRET_ASSIGNMENT = /(?:access[_-]?token|refresh[_-]?token|api[_-]?key|client[_-]?secret|secret|password|credential|authorization|cookie)\s*(?:=|:)\s*[^\s&#;,]+/i;
17
17
  const URI_USERINFO = /^[a-z][a-z0-9+.-]*:\/\/[^/?#\s]*:[^@/?#\s]+@/i;
18
+ export const ONBOARDING_BLOCK_PATTERNS = Object.freeze([
19
+ { name: 'openai_api_key', regex: /sk-[A-Za-z0-9]{20,}/g },
20
+ { name: 'github_pat', regex: /ghp_[A-Za-z0-9]{20,}/g },
21
+ { name: 'anthropic_api_key', regex: /sk-ant-[A-Za-z0-9_-]{20,}/g },
22
+ { name: 'aws_access_key', regex: /AKIA[0-9A-Z]{16}/g },
23
+ { name: 'slack_token', regex: /xox[abpsr]-[A-Za-z0-9-]{10,}/g },
24
+ { name: 'private_key_block', regex: /-----BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY-----/g },
25
+ { name: 'password_assignment', regex: /(?:password|passwd|secret)\s*=\s*['"][^'"\s]{6,}['"]/gi },
26
+ { name: 'infisical_ref', regex: /infisical:\/\/[^\s]+/gi }
27
+ ]);
28
+ export const ONBOARDING_PII_PATTERNS = Object.freeze([
29
+ { name: 'jp_phone', regex: /0\d{1,4}-\d{1,4}-\d{3,4}/g },
30
+ { name: 'email', regex: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g },
31
+ { name: 'credit_card_like', regex: /\b(?:\d{4}[- ]?){3}\d{4}\b/g }
32
+ ]);
33
+ /** Scan candidate text without exposing raw content in the finding output. */
34
+ export function scanOnboardingContent(content) {
35
+ if (typeof content !== 'string' || content.length === 0) {
36
+ return { block: false, review: false, findings: [] };
37
+ }
38
+ const findings = [];
39
+ for (const { name, regex } of ONBOARDING_BLOCK_PATTERNS) {
40
+ const matches = content.match(regex);
41
+ if (matches && matches.length > 0)
42
+ findings.push({ kind: 'block', name, count: matches.length });
43
+ }
44
+ for (const { name, regex } of ONBOARDING_PII_PATTERNS) {
45
+ const matches = content.match(regex);
46
+ if (matches && matches.length > 0)
47
+ findings.push({ kind: 'review', name, count: matches.length });
48
+ }
49
+ return {
50
+ block: findings.some((finding) => finding.kind === 'block'),
51
+ review: findings.some((finding) => finding.kind === 'review'),
52
+ findings
53
+ };
54
+ }
18
55
  const PERMISSION_KEYS = new Set([
19
56
  'visibility', 'collectedBy', 'provider', 'connectionId', 'grantId', 'accountId',
20
57
  'folderId', 'projectCode', 'roleMin', 'sensitivity', 'scope', 'scopes', 'roles',