@uxmaltech/collab-cli 0.1.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 (109) hide show
  1. package/README.md +227 -0
  2. package/bin/collab +10 -0
  3. package/dist/cli.js +34 -0
  4. package/dist/commands/canon/index.js +16 -0
  5. package/dist/commands/canon/rebuild.js +95 -0
  6. package/dist/commands/compose/generate.js +63 -0
  7. package/dist/commands/compose/index.js +18 -0
  8. package/dist/commands/compose/validate.js +53 -0
  9. package/dist/commands/doctor.js +153 -0
  10. package/dist/commands/index.js +27 -0
  11. package/dist/commands/infra/down.js +23 -0
  12. package/dist/commands/infra/index.js +20 -0
  13. package/dist/commands/infra/shared.js +59 -0
  14. package/dist/commands/infra/status.js +64 -0
  15. package/dist/commands/infra/up.js +29 -0
  16. package/dist/commands/init.js +830 -0
  17. package/dist/commands/mcp/index.js +20 -0
  18. package/dist/commands/mcp/shared.js +57 -0
  19. package/dist/commands/mcp/start.js +45 -0
  20. package/dist/commands/mcp/status.js +62 -0
  21. package/dist/commands/mcp/stop.js +23 -0
  22. package/dist/commands/seed.js +55 -0
  23. package/dist/commands/uninstall.js +36 -0
  24. package/dist/commands/up.js +78 -0
  25. package/dist/commands/update-canons.js +48 -0
  26. package/dist/commands/upgrade.js +54 -0
  27. package/dist/index.js +14 -0
  28. package/dist/lib/ai-client.js +317 -0
  29. package/dist/lib/ansi.js +58 -0
  30. package/dist/lib/canon-index-generator.js +64 -0
  31. package/dist/lib/canon-index-targets.js +68 -0
  32. package/dist/lib/canon-resolver.js +262 -0
  33. package/dist/lib/canon-scaffold.js +57 -0
  34. package/dist/lib/cli-detection.js +149 -0
  35. package/dist/lib/command-context.js +23 -0
  36. package/dist/lib/compose-defaults.js +47 -0
  37. package/dist/lib/compose-env.js +24 -0
  38. package/dist/lib/compose-paths.js +36 -0
  39. package/dist/lib/compose-renderer.js +134 -0
  40. package/dist/lib/compose-validator.js +56 -0
  41. package/dist/lib/config.js +195 -0
  42. package/dist/lib/credentials.js +63 -0
  43. package/dist/lib/docker-checks.js +73 -0
  44. package/dist/lib/docker-compose.js +15 -0
  45. package/dist/lib/docker-status.js +151 -0
  46. package/dist/lib/domain-gen.js +376 -0
  47. package/dist/lib/ecosystem.js +150 -0
  48. package/dist/lib/env-file.js +77 -0
  49. package/dist/lib/errors.js +30 -0
  50. package/dist/lib/executor.js +85 -0
  51. package/dist/lib/github-auth.js +204 -0
  52. package/dist/lib/hash.js +7 -0
  53. package/dist/lib/health-checker.js +140 -0
  54. package/dist/lib/logger.js +87 -0
  55. package/dist/lib/mcp-client.js +88 -0
  56. package/dist/lib/mode.js +36 -0
  57. package/dist/lib/model-listing.js +102 -0
  58. package/dist/lib/model-registry.js +55 -0
  59. package/dist/lib/npm-operations.js +69 -0
  60. package/dist/lib/orchestrator.js +170 -0
  61. package/dist/lib/parsers.js +42 -0
  62. package/dist/lib/port-resolver.js +57 -0
  63. package/dist/lib/preconditions.js +35 -0
  64. package/dist/lib/preflight.js +88 -0
  65. package/dist/lib/process.js +6 -0
  66. package/dist/lib/prompt.js +125 -0
  67. package/dist/lib/providers.js +117 -0
  68. package/dist/lib/repo-analysis-helpers.js +379 -0
  69. package/dist/lib/repo-scanner.js +195 -0
  70. package/dist/lib/service-health.js +79 -0
  71. package/dist/lib/shell.js +49 -0
  72. package/dist/lib/state.js +38 -0
  73. package/dist/lib/update-checker.js +130 -0
  74. package/dist/lib/version.js +27 -0
  75. package/dist/stages/agent-skills-setup.js +301 -0
  76. package/dist/stages/assistant-setup.js +325 -0
  77. package/dist/stages/canon-ingest.js +249 -0
  78. package/dist/stages/canon-rebuild-graph.js +33 -0
  79. package/dist/stages/canon-rebuild-indexes.js +40 -0
  80. package/dist/stages/canon-rebuild-snapshot.js +75 -0
  81. package/dist/stages/canon-rebuild-validate.js +57 -0
  82. package/dist/stages/canon-rebuild-vectors.js +30 -0
  83. package/dist/stages/canon-scaffold.js +15 -0
  84. package/dist/stages/canon-sync.js +49 -0
  85. package/dist/stages/ci-setup.js +56 -0
  86. package/dist/stages/domain-gen.js +363 -0
  87. package/dist/stages/graph-seed.js +26 -0
  88. package/dist/stages/repo-analysis-fileonly.js +111 -0
  89. package/dist/stages/repo-analysis.js +112 -0
  90. package/dist/stages/repo-scaffold.js +110 -0
  91. package/dist/templates/canon/contracts-readme.js +39 -0
  92. package/dist/templates/canon/domain-readme.js +40 -0
  93. package/dist/templates/canon/evolution/changelog.js +53 -0
  94. package/dist/templates/canon/governance/confidence-levels.js +38 -0
  95. package/dist/templates/canon/governance/implementation-process.js +34 -0
  96. package/dist/templates/canon/governance/review-process.js +29 -0
  97. package/dist/templates/canon/governance/schema-versioning.js +25 -0
  98. package/dist/templates/canon/governance/what-enters-the-canon.js +44 -0
  99. package/dist/templates/canon/index.js +28 -0
  100. package/dist/templates/canon/knowledge-readme.js +129 -0
  101. package/dist/templates/canon/system-prompt.js +101 -0
  102. package/dist/templates/ci/architecture-merge.js +29 -0
  103. package/dist/templates/ci/architecture-pr.js +26 -0
  104. package/dist/templates/ci/index.js +7 -0
  105. package/dist/templates/consolidated.js +114 -0
  106. package/dist/templates/infra.js +90 -0
  107. package/dist/templates/mcp.js +32 -0
  108. package/install.sh +455 -0
  109. package/package.json +48 -0
@@ -0,0 +1,125 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.promptChoice = promptChoice;
7
+ exports.promptBoolean = promptBoolean;
8
+ exports.promptMultiSelect = promptMultiSelect;
9
+ exports.promptText = promptText;
10
+ exports.promptPassword = promptPassword;
11
+ const promises_1 = __importDefault(require("node:readline/promises"));
12
+ const node_process_1 = require("node:process");
13
+ async function promptChoice(question, choices, defaultValue) {
14
+ const rl = promises_1.default.createInterface({ input: node_process_1.stdin, output: node_process_1.stdout });
15
+ try {
16
+ const menu = choices
17
+ .map((choice, index) => {
18
+ const marker = choice.value === defaultValue ? ' (default)' : '';
19
+ return `${index + 1}. ${choice.label}${marker}`;
20
+ })
21
+ .join('\n');
22
+ const answer = await rl.question(`${question}\n${menu}\n> `);
23
+ const trimmed = answer.trim();
24
+ if (!trimmed) {
25
+ return defaultValue;
26
+ }
27
+ const index = Number.parseInt(trimmed, 10);
28
+ if (!Number.isNaN(index) && index >= 1 && index <= choices.length) {
29
+ return choices[index - 1].value;
30
+ }
31
+ const byValue = choices.find((choice) => choice.value === trimmed);
32
+ if (byValue) {
33
+ return byValue.value;
34
+ }
35
+ return defaultValue;
36
+ }
37
+ finally {
38
+ rl.close();
39
+ }
40
+ }
41
+ async function promptBoolean(question, defaultValue) {
42
+ const rl = promises_1.default.createInterface({ input: node_process_1.stdin, output: node_process_1.stdout });
43
+ try {
44
+ const suffix = defaultValue ? '[Y/n]' : '[y/N]';
45
+ const answer = await rl.question(`${question} ${suffix} `);
46
+ const trimmed = answer.trim().toLowerCase();
47
+ if (!trimmed) {
48
+ return defaultValue;
49
+ }
50
+ if (trimmed === 'y' || trimmed === 'yes') {
51
+ return true;
52
+ }
53
+ if (trimmed === 'n' || trimmed === 'no') {
54
+ return false;
55
+ }
56
+ return defaultValue;
57
+ }
58
+ finally {
59
+ rl.close();
60
+ }
61
+ }
62
+ async function promptMultiSelect(question, choices, defaults = []) {
63
+ const rl = promises_1.default.createInterface({ input: node_process_1.stdin, output: node_process_1.stdout });
64
+ try {
65
+ const defaultSet = new Set(defaults);
66
+ const menu = choices
67
+ .map((choice, index) => {
68
+ const marker = defaultSet.has(choice.value) ? ' (default)' : '';
69
+ const desc = choice.description ? ` — ${choice.description}` : '';
70
+ return `${index + 1}. ${choice.label}${desc}${marker}`;
71
+ })
72
+ .join('\n');
73
+ const hint = 'Enter numbers separated by commas, * for all, or empty for defaults';
74
+ const answer = await rl.question(`${question}\n${menu}\n(${hint})\n> `);
75
+ const trimmed = answer.trim();
76
+ if (!trimmed) {
77
+ return defaults.length > 0 ? [...defaults] : [];
78
+ }
79
+ if (trimmed === '*') {
80
+ return choices.map((c) => c.value);
81
+ }
82
+ const selected = [];
83
+ const parts = trimmed.split(',').map((p) => p.trim());
84
+ for (const part of parts) {
85
+ const index = Number.parseInt(part, 10);
86
+ if (!Number.isNaN(index) && index >= 1 && index <= choices.length) {
87
+ const value = choices[index - 1].value;
88
+ if (!selected.includes(value)) {
89
+ selected.push(value);
90
+ }
91
+ continue;
92
+ }
93
+ const byValue = choices.find((c) => c.value === part);
94
+ if (byValue && !selected.includes(byValue.value)) {
95
+ selected.push(byValue.value);
96
+ }
97
+ }
98
+ return selected.length > 0 ? selected : defaults.length > 0 ? [...defaults] : [];
99
+ }
100
+ finally {
101
+ rl.close();
102
+ }
103
+ }
104
+ async function promptText(question, defaultValue) {
105
+ const rl = promises_1.default.createInterface({ input: node_process_1.stdin, output: node_process_1.stdout });
106
+ try {
107
+ const suffix = defaultValue ? ` (${defaultValue})` : '';
108
+ const answer = await rl.question(`${question}${suffix}\n> `);
109
+ const trimmed = answer.trim();
110
+ return trimmed || defaultValue || '';
111
+ }
112
+ finally {
113
+ rl.close();
114
+ }
115
+ }
116
+ async function promptPassword(question) {
117
+ const rl = promises_1.default.createInterface({ input: node_process_1.stdin, output: node_process_1.stdout });
118
+ try {
119
+ const answer = await rl.question(`${question}\n> `);
120
+ return answer;
121
+ }
122
+ finally {
123
+ rl.close();
124
+ }
125
+ }
@@ -0,0 +1,117 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.PROVIDER_DEFAULTS = exports.PROVIDER_KEYS = void 0;
37
+ exports.isProviderKey = isProviderKey;
38
+ exports.parseProviderList = parseProviderList;
39
+ exports.getEnabledProviders = getEnabledProviders;
40
+ exports.autoDetectProviders = autoDetectProviders;
41
+ exports.PROVIDER_KEYS = ['codex', 'claude', 'gemini', 'copilot'];
42
+ exports.PROVIDER_DEFAULTS = {
43
+ codex: {
44
+ label: 'Codex (OpenAI)',
45
+ description: 'OpenAI models via codex CLI or API key',
46
+ envVar: 'OPENAI_API_KEY',
47
+ models: ['o3-pro', 'gpt-4.1', 'o4-mini'],
48
+ },
49
+ claude: {
50
+ label: 'Claude (Anthropic)',
51
+ description: 'Anthropic models via claude CLI or API key',
52
+ envVar: 'ANTHROPIC_API_KEY',
53
+ models: ['claude-sonnet-4-20250514', 'claude-opus-4-20250514', 'claude-haiku-35-20241022'],
54
+ },
55
+ gemini: {
56
+ label: 'Gemini (Google)',
57
+ description: 'Google AI models via gemini CLI or API key',
58
+ envVar: 'GOOGLE_AI_API_KEY',
59
+ models: ['gemini-2.5-pro', 'gemini-2.5-flash'],
60
+ },
61
+ copilot: {
62
+ label: 'Copilot (GitHub)',
63
+ description: 'GitHub Copilot via gh CLI — uses issues assigned to @copilot',
64
+ envVar: '',
65
+ models: [],
66
+ },
67
+ };
68
+ const PROVIDER_SET = new Set(exports.PROVIDER_KEYS);
69
+ function isProviderKey(value) {
70
+ return PROVIDER_SET.has(value);
71
+ }
72
+ function parseProviderList(value) {
73
+ const keys = value
74
+ .split(',')
75
+ .map((s) => s.trim().toLowerCase())
76
+ .filter((s) => s.length > 0);
77
+ const result = [];
78
+ for (const key of keys) {
79
+ if (!isProviderKey(key)) {
80
+ throw new Error(`Invalid provider '${key}'. Valid providers: ${exports.PROVIDER_KEYS.join(', ')}`);
81
+ }
82
+ if (!result.includes(key)) {
83
+ result.push(key);
84
+ }
85
+ }
86
+ return result;
87
+ }
88
+ function getEnabledProviders(config) {
89
+ const assistants = config.assistants;
90
+ if (!assistants?.providers) {
91
+ return [];
92
+ }
93
+ return exports.PROVIDER_KEYS.filter((key) => assistants.providers[key]?.enabled === true);
94
+ }
95
+ /**
96
+ * Auto-detects providers from environment variables and installed CLIs.
97
+ * A provider is detected if its env var is set OR its CLI is on PATH.
98
+ */
99
+ async function autoDetectProviders() {
100
+ // Lazy import to avoid circular dependency at module level
101
+ const { detectProviderCli } = await Promise.resolve().then(() => __importStar(require('./cli-detection')));
102
+ const detected = [];
103
+ for (const key of exports.PROVIDER_KEYS) {
104
+ const defaults = exports.PROVIDER_DEFAULTS[key];
105
+ // Copilot has no env var — detected only via gh CLI
106
+ if (defaults.envVar && process.env[defaults.envVar]) {
107
+ detected.push(key);
108
+ continue;
109
+ }
110
+ // Check if the provider CLI is installed
111
+ const cli = detectProviderCli(key);
112
+ if (cli.available) {
113
+ detected.push(key);
114
+ }
115
+ }
116
+ return detected;
117
+ }
@@ -0,0 +1,379 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.PLACEHOLDER_MARKER = void 0;
7
+ exports.buildUserMessage = buildUserMessage;
8
+ exports.canOverwrite = canOverwrite;
9
+ exports.extractJson = extractJson;
10
+ exports.sanitizeId = sanitizeId;
11
+ exports.renderAxiom = renderAxiom;
12
+ exports.renderDecision = renderDecision;
13
+ exports.renderConvention = renderConvention;
14
+ exports.renderAntiPattern = renderAntiPattern;
15
+ exports.renderDomain = renderDomain;
16
+ exports.writeAnalysisResults = writeAnalysisResults;
17
+ exports.generateAiHelpers = generateAiHelpers;
18
+ const node_fs_1 = __importDefault(require("node:fs"));
19
+ const node_path_1 = __importDefault(require("node:path"));
20
+ exports.PLACEHOLDER_MARKER = '<!-- AI-GENERATED: PLACEHOLDER -->';
21
+ /**
22
+ * Builds the user message with the repository context.
23
+ */
24
+ function buildUserMessage(repoCtx) {
25
+ const parts = [
26
+ `# Repository: ${repoCtx.name}`,
27
+ '',
28
+ `**Language:** ${repoCtx.language}`,
29
+ repoCtx.framework ? `**Framework:** ${repoCtx.framework}` : null,
30
+ `**Source files:** ${repoCtx.totalSourceFiles}`,
31
+ '',
32
+ '## Dependencies',
33
+ repoCtx.dependencies.length > 0
34
+ ? repoCtx.dependencies.map((d) => `- ${d}`).join('\n')
35
+ : '_No dependencies detected._',
36
+ '',
37
+ '## Key Files',
38
+ repoCtx.keyFiles.map((f) => `- ${f}`).join('\n'),
39
+ '',
40
+ '## Directory Structure',
41
+ '```',
42
+ repoCtx.structure,
43
+ '```',
44
+ '',
45
+ 'Analyze this repository and generate the canonical architecture documentation.',
46
+ ];
47
+ return parts.filter((p) => p !== null).join('\n');
48
+ }
49
+ /**
50
+ * Checks if a file can be overwritten by the AI analysis.
51
+ * Only files containing the PLACEHOLDER_MARKER are overwritten.
52
+ */
53
+ function canOverwrite(filePath) {
54
+ if (!node_fs_1.default.existsSync(filePath)) {
55
+ return true;
56
+ }
57
+ const content = node_fs_1.default.readFileSync(filePath, 'utf8');
58
+ return content.includes(exports.PLACEHOLDER_MARKER);
59
+ }
60
+ /**
61
+ * Extracts JSON from a response that may contain markdown code fences.
62
+ */
63
+ function extractJson(text) {
64
+ const fenceMatch = text.match(/```(?:json)?\s*\n([\s\S]*?)\n```/);
65
+ if (fenceMatch) {
66
+ return fenceMatch[1].trim();
67
+ }
68
+ const braceStart = text.indexOf('{');
69
+ const braceEnd = text.lastIndexOf('}');
70
+ if (braceStart >= 0 && braceEnd > braceStart) {
71
+ return text.slice(braceStart, braceEnd + 1);
72
+ }
73
+ return text;
74
+ }
75
+ /**
76
+ * Sanitizes an ID for use as a filename.
77
+ */
78
+ function sanitizeId(id) {
79
+ return id.replace(/[^a-zA-Z0-9-]/g, '-');
80
+ }
81
+ /**
82
+ * Safely coerces an unknown value to a string.
83
+ * Used for AI-generated JSON fields that may be objects instead of strings.
84
+ */
85
+ function str(value) {
86
+ if (typeof value === 'string') {
87
+ return value;
88
+ }
89
+ if (value == null) {
90
+ return '';
91
+ }
92
+ return JSON.stringify(value);
93
+ }
94
+ function renderAxiom(entry) {
95
+ return [
96
+ `# ${entry.id}: ${entry.title}`,
97
+ '',
98
+ `**Confidence:** ${entry.confidence ?? 'MEDIUM'}`,
99
+ `**Verified:** ${new Date().toISOString().split('T')[0]}`,
100
+ '',
101
+ '## Statement',
102
+ '',
103
+ str(entry.statement ?? entry.title),
104
+ '',
105
+ '## Rationale',
106
+ '',
107
+ str(entry.rationale ?? '_Not provided._'),
108
+ '',
109
+ '## Verification',
110
+ '',
111
+ str(entry.verification ?? '_Pending verification._'),
112
+ '',
113
+ '<!-- AI-GENERATED -->',
114
+ '',
115
+ ].join('\n');
116
+ }
117
+ function renderDecision(entry) {
118
+ return [
119
+ `# ${entry.id}: ${entry.title}`,
120
+ '',
121
+ `**Status:** ${str(entry.status ?? 'Accepted')}`,
122
+ `**Date:** ${new Date().toISOString().split('T')[0]}`,
123
+ `**Confidence:** ${entry.confidence ?? 'MEDIUM'}`,
124
+ '',
125
+ '## Context',
126
+ '',
127
+ str(entry.context ?? '_Not provided._'),
128
+ '',
129
+ '## Decision',
130
+ '',
131
+ str(entry.decision ?? '_Not provided._'),
132
+ '',
133
+ '## Consequences',
134
+ '',
135
+ str(entry.consequences ?? '_Not provided._'),
136
+ '',
137
+ '<!-- AI-GENERATED -->',
138
+ '',
139
+ ].join('\n');
140
+ }
141
+ function renderConvention(entry) {
142
+ return [
143
+ `# ${entry.id}: ${entry.title}`,
144
+ '',
145
+ `**Confidence:** ${entry.confidence ?? 'MEDIUM'}`,
146
+ `**Scope:** ${str(entry.scope ?? 'project')}`,
147
+ '',
148
+ '## Convention',
149
+ '',
150
+ str(entry.convention ?? entry.title),
151
+ '',
152
+ '## Examples',
153
+ '',
154
+ str(entry.examples ?? '_See codebase._'),
155
+ '',
156
+ '## Rationale',
157
+ '',
158
+ str(entry.rationale ?? '_Not provided._'),
159
+ '',
160
+ '<!-- AI-GENERATED -->',
161
+ '',
162
+ ].join('\n');
163
+ }
164
+ function renderAntiPattern(entry) {
165
+ return [
166
+ `# ${entry.id}: ${entry.title}`,
167
+ '',
168
+ `**Confidence:** ${entry.confidence ?? 'MEDIUM'}`,
169
+ `**Severity:** ${str(entry.severity ?? 'warning')}`,
170
+ '',
171
+ '## Problem',
172
+ '',
173
+ str(entry.problem ?? entry.title),
174
+ '',
175
+ '## Why It\'s Harmful',
176
+ '',
177
+ str(entry.harm ?? '_Not provided._'),
178
+ '',
179
+ '## Alternative',
180
+ '',
181
+ str(entry.alternative ?? '_Not provided._'),
182
+ '',
183
+ '<!-- AI-GENERATED -->',
184
+ '',
185
+ ].join('\n');
186
+ }
187
+ function renderDomain(entry) {
188
+ return [
189
+ `# Domain: ${entry.name}`,
190
+ '',
191
+ `**Confidence:** ${entry.confidence ?? 'MEDIUM'}`,
192
+ '',
193
+ '## Responsibilities',
194
+ '',
195
+ String(entry.responsibilities ?? '_Not provided._'),
196
+ '',
197
+ '## Boundaries',
198
+ '',
199
+ String(entry.boundaries ?? '_Not provided._'),
200
+ '',
201
+ '## Dependencies',
202
+ '',
203
+ String(entry.dependencies ?? '_None._'),
204
+ '',
205
+ '## Public API',
206
+ '',
207
+ String(entry.publicApi ?? '_Not defined._'),
208
+ '',
209
+ '<!-- AI-GENERATED -->',
210
+ '',
211
+ ].join('\n');
212
+ }
213
+ /**
214
+ * Writes analysis results to a base directory.
215
+ * Used by both indexed (architectureDir) and file-only (repoDir) stages.
216
+ */
217
+ function writeAnalysisResults(ctx, baseDir, result) {
218
+ let written = 0;
219
+ if (result.axioms) {
220
+ for (const entry of result.axioms) {
221
+ const filename = `${sanitizeId(entry.id)}-${sanitizeId(entry.title).toLowerCase().slice(0, 40)}.md`;
222
+ const filePath = node_path_1.default.join(baseDir, 'knowledge', 'axioms', filename);
223
+ if (canOverwrite(filePath)) {
224
+ ctx.executor.ensureDirectory(node_path_1.default.dirname(filePath));
225
+ ctx.executor.writeFile(filePath, renderAxiom(entry), { description: `write axiom ${entry.id}` });
226
+ written++;
227
+ }
228
+ }
229
+ }
230
+ if (result.decisions) {
231
+ for (const entry of result.decisions) {
232
+ const filename = `${sanitizeId(entry.id)}-${sanitizeId(entry.title).toLowerCase().slice(0, 40)}.md`;
233
+ const filePath = node_path_1.default.join(baseDir, 'knowledge', 'decisions', filename);
234
+ if (canOverwrite(filePath)) {
235
+ ctx.executor.ensureDirectory(node_path_1.default.dirname(filePath));
236
+ ctx.executor.writeFile(filePath, renderDecision(entry), { description: `write decision ${entry.id}` });
237
+ written++;
238
+ }
239
+ }
240
+ }
241
+ if (result.conventions) {
242
+ for (const entry of result.conventions) {
243
+ const filename = `${sanitizeId(entry.id)}-${sanitizeId(entry.title).toLowerCase().slice(0, 40)}.md`;
244
+ const filePath = node_path_1.default.join(baseDir, 'knowledge', 'conventions', filename);
245
+ if (canOverwrite(filePath)) {
246
+ ctx.executor.ensureDirectory(node_path_1.default.dirname(filePath));
247
+ ctx.executor.writeFile(filePath, renderConvention(entry), { description: `write convention ${entry.id}` });
248
+ written++;
249
+ }
250
+ }
251
+ }
252
+ if (result.antiPatterns) {
253
+ for (const entry of result.antiPatterns) {
254
+ const filename = `${sanitizeId(entry.id)}-${sanitizeId(entry.title).toLowerCase().slice(0, 40)}.md`;
255
+ const filePath = node_path_1.default.join(baseDir, 'knowledge', 'anti-patterns', filename);
256
+ if (canOverwrite(filePath)) {
257
+ ctx.executor.ensureDirectory(node_path_1.default.dirname(filePath));
258
+ ctx.executor.writeFile(filePath, renderAntiPattern(entry), { description: `write anti-pattern ${entry.id}` });
259
+ written++;
260
+ }
261
+ }
262
+ }
263
+ if (result.domains) {
264
+ for (const entry of result.domains) {
265
+ const safeName = sanitizeId(entry.name).toLowerCase();
266
+ const filename = `${safeName}.md`;
267
+ const filePath = node_path_1.default.join(baseDir, 'domains', filename);
268
+ if (canOverwrite(filePath)) {
269
+ ctx.executor.ensureDirectory(node_path_1.default.dirname(filePath));
270
+ ctx.executor.writeFile(filePath, renderDomain(entry), { description: `write domain ${entry.name}` });
271
+ written++;
272
+ }
273
+ }
274
+ }
275
+ ctx.logger.info(`Repository analysis: ${written} architecture file(s) written.`);
276
+ }
277
+ /**
278
+ * Generates docs/ai/ helper files for fast agent reference.
279
+ * These are lightweight summaries that prevent agents from scanning the full architecture tree.
280
+ * Used by both file-only and indexed pipelines.
281
+ */
282
+ function generateAiHelpers(ctx, repoCtx, analysis) {
283
+ const aiDir = ctx.config.aiDir;
284
+ ctx.executor.ensureDirectory(aiDir);
285
+ // 00_brief.md — one-paragraph project summary
286
+ const briefContent = [
287
+ '# Project Brief',
288
+ '',
289
+ `**Name:** ${repoCtx.name}`,
290
+ `**Language:** ${repoCtx.language}`,
291
+ repoCtx.framework ? `**Framework:** ${repoCtx.framework}` : null,
292
+ `**Source files:** ${repoCtx.totalSourceFiles}`,
293
+ '',
294
+ '## Summary',
295
+ '',
296
+ `This is a ${repoCtx.language}${repoCtx.framework ? ` / ${repoCtx.framework}` : ''} project` +
297
+ ` with ${repoCtx.totalSourceFiles} source files.`,
298
+ '',
299
+ '<!-- AI-GENERATED -->',
300
+ '',
301
+ ].filter((l) => l !== null).join('\n');
302
+ ctx.executor.writeFile(node_path_1.default.join(aiDir, '00_brief.md'), briefContent, {
303
+ description: 'write AI helper: project brief',
304
+ });
305
+ // 01_domain_map.md — domain index
306
+ const domainLines = ['# Domain Map', ''];
307
+ if (analysis.domains && analysis.domains.length > 0) {
308
+ for (const d of analysis.domains) {
309
+ domainLines.push(`## ${d.name}`);
310
+ domainLines.push('');
311
+ if (d.responsibilities) {
312
+ domainLines.push(`**Responsibilities:** ${d.responsibilities}`);
313
+ }
314
+ if (d.boundaries) {
315
+ domainLines.push(`**Boundaries:** ${d.boundaries}`);
316
+ }
317
+ domainLines.push('');
318
+ }
319
+ }
320
+ else {
321
+ domainLines.push('_No domains detected yet. Run repository analysis with an AI provider._');
322
+ domainLines.push('');
323
+ }
324
+ domainLines.push('<!-- AI-GENERATED -->');
325
+ domainLines.push('');
326
+ ctx.executor.writeFile(node_path_1.default.join(aiDir, '01_domain_map.md'), domainLines.join('\n'), {
327
+ description: 'write AI helper: domain map',
328
+ });
329
+ // 02_module_map.md — key files and structure
330
+ const moduleLines = [
331
+ '# Module Map',
332
+ '',
333
+ '## Key Files',
334
+ '',
335
+ ...repoCtx.keyFiles.map((f) => `- \`${f}\``),
336
+ '',
337
+ '## Directory Structure',
338
+ '',
339
+ '```',
340
+ repoCtx.structure,
341
+ '```',
342
+ '',
343
+ '## Dependencies',
344
+ '',
345
+ ...(repoCtx.dependencies.length > 0
346
+ ? repoCtx.dependencies.map((d) => `- ${d}`)
347
+ : ['_No dependencies detected._']),
348
+ '',
349
+ '<!-- AI-GENERATED -->',
350
+ '',
351
+ ];
352
+ ctx.executor.writeFile(node_path_1.default.join(aiDir, '02_module_map.md'), moduleLines.join('\n'), {
353
+ description: 'write AI helper: module map',
354
+ });
355
+ // _snapshot.md — quick-reference index linking to the other files
356
+ const snapshotLines = [
357
+ '# Architecture Snapshot',
358
+ '',
359
+ `Generated: ${new Date().toISOString().split('T')[0]}`,
360
+ '',
361
+ '## Quick Links',
362
+ '',
363
+ '- [Project Brief](./00_brief.md)',
364
+ '- [Domain Map](./01_domain_map.md)',
365
+ '- [Module Map](./02_module_map.md)',
366
+ '',
367
+ '## Architecture Sources',
368
+ '',
369
+ '- `docs/architecture/uxmaltech/` — Institutional canon (collab-architecture copy)',
370
+ '- `docs/architecture/repo/` — Project-specific canons',
371
+ '',
372
+ '<!-- AI-GENERATED -->',
373
+ '',
374
+ ];
375
+ ctx.executor.writeFile(node_path_1.default.join(aiDir, '_snapshot.md'), snapshotLines.join('\n'), {
376
+ description: 'write AI helper: snapshot index',
377
+ });
378
+ ctx.logger.info(`AI helpers: 4 file(s) written to docs/ai/.`);
379
+ }