prjct-cli 1.9.0 → 1.11.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.
@@ -0,0 +1,226 @@
1
+ /**
2
+ * Token Budget Coordinator
3
+ *
4
+ * Centrally manages the global token budget across all context-loading components.
5
+ * Ensures the combined prompt stays within the model's context window
6
+ * and reserves space for the output.
7
+ *
8
+ * Budget formula: inputBudget = modelContextWindow * 0.65
9
+ * Priority: state (P1) > injection context (P2) > file content (P3)
10
+ *
11
+ * @see PRJ-266
12
+ */
13
+
14
+ // =============================================================================
15
+ // Model Context Windows
16
+ // =============================================================================
17
+
18
+ /** Context window sizes by model identifier (in tokens) */
19
+ export const MODEL_CONTEXT_WINDOWS: Record<string, number> = {
20
+ // Claude models (short names from model.ts)
21
+ opus: 200_000,
22
+ sonnet: 200_000,
23
+ haiku: 200_000,
24
+ // Gemini models
25
+ '2.5-pro': 1_000_000,
26
+ '2.5-flash': 1_000_000,
27
+ '2.0-flash': 1_000_000,
28
+ // Full model IDs (for direct API usage)
29
+ 'claude-opus-4.5': 200_000,
30
+ 'claude-sonnet-4.5': 200_000,
31
+ 'claude-haiku-4.5': 200_000,
32
+ 'claude-opus-4-6': 200_000,
33
+ // Default fallback
34
+ default: 200_000,
35
+ }
36
+
37
+ /** Ratio of context window reserved for input (rest for output) */
38
+ export const INPUT_RATIO = 0.65
39
+
40
+ // =============================================================================
41
+ // Budget Categories
42
+ // =============================================================================
43
+
44
+ /** Budget category identifiers ordered by priority */
45
+ export type BudgetCategory = 'state' | 'injection' | 'files'
46
+
47
+ /** Budget allocation result for each category */
48
+ export interface BudgetAllocation {
49
+ state: number
50
+ injection: number
51
+ files: number
52
+ inputBudget: number
53
+ outputReserve: number
54
+ contextWindow: number
55
+ }
56
+
57
+ /** Usage tracking per category */
58
+ export interface BudgetUsage {
59
+ category: BudgetCategory
60
+ allocated: number
61
+ used: number
62
+ remaining: number
63
+ }
64
+
65
+ // =============================================================================
66
+ // Default Allocation Ratios (within input budget)
67
+ // =============================================================================
68
+
69
+ interface AllocationConfig {
70
+ ratio: number
71
+ minimum: number
72
+ }
73
+
74
+ const ALLOCATION_CONFIG: Record<BudgetCategory, AllocationConfig> = {
75
+ /** P1: State — current task, queue, patterns (highest priority) */
76
+ state: { ratio: 0.02, minimum: 1_500 },
77
+ /** P2: Injection — agents, skills, modules, checklists */
78
+ injection: { ratio: 0.08, minimum: 8_000 },
79
+ /** P3: Files — codebase file content (lowest priority, gets remainder) */
80
+ files: { ratio: 0.9, minimum: 20_000 },
81
+ }
82
+
83
+ /** Priority order for budget distribution */
84
+ const PRIORITY_ORDER: BudgetCategory[] = ['state', 'injection', 'files']
85
+
86
+ // =============================================================================
87
+ // TokenBudgetCoordinator
88
+ // =============================================================================
89
+
90
+ export class TokenBudgetCoordinator {
91
+ private readonly _contextWindow: number
92
+ private readonly _inputBudget: number
93
+ private readonly _outputReserve: number
94
+ private readonly _allocations: Map<BudgetCategory, number> = new Map()
95
+ private readonly _used: Map<BudgetCategory, number> = new Map()
96
+
97
+ constructor(model?: string) {
98
+ this._contextWindow = getContextWindow(model)
99
+ this._inputBudget = Math.floor(this._contextWindow * INPUT_RATIO)
100
+ this._outputReserve = this._contextWindow - this._inputBudget
101
+ this.distributeBudget()
102
+ }
103
+
104
+ /** Distribute input budget across categories by priority */
105
+ private distributeBudget(): void {
106
+ let remaining = this._inputBudget
107
+
108
+ for (const category of PRIORITY_ORDER) {
109
+ const config = ALLOCATION_CONFIG[category]
110
+
111
+ if (category === 'files') {
112
+ // Lowest priority gets whatever remains
113
+ this._allocations.set(category, Math.max(remaining, 0))
114
+ } else {
115
+ const allocation = Math.max(config.minimum, Math.floor(this._inputBudget * config.ratio))
116
+ const granted = Math.min(allocation, remaining)
117
+ this._allocations.set(category, granted)
118
+ remaining -= granted
119
+ }
120
+
121
+ this._used.set(category, 0)
122
+ }
123
+ }
124
+
125
+ /** Get the full budget allocation */
126
+ getAllocation(): BudgetAllocation {
127
+ return {
128
+ state: this._allocations.get('state') ?? 0,
129
+ injection: this._allocations.get('injection') ?? 0,
130
+ files: this._allocations.get('files') ?? 0,
131
+ inputBudget: this._inputBudget,
132
+ outputReserve: this._outputReserve,
133
+ contextWindow: this._contextWindow,
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Request tokens from a category.
139
+ * Returns actual tokens granted (may be less than requested if budget is exhausted).
140
+ */
141
+ request(category: BudgetCategory, requestedTokens: number): number {
142
+ const allocated = this._allocations.get(category) ?? 0
143
+ const currentUsed = this._used.get(category) ?? 0
144
+ const available = Math.max(0, allocated - currentUsed)
145
+ const granted = Math.min(requestedTokens, available)
146
+ this._used.set(category, currentUsed + granted)
147
+ return granted
148
+ }
149
+
150
+ /** Record token usage for a category */
151
+ record(category: BudgetCategory, tokensUsed: number): void {
152
+ const current = this._used.get(category) ?? 0
153
+ this._used.set(category, current + tokensUsed)
154
+ }
155
+
156
+ /** Get usage details for a category */
157
+ getUsage(category: BudgetCategory): BudgetUsage {
158
+ const allocated = this._allocations.get(category) ?? 0
159
+ const used = this._used.get(category) ?? 0
160
+ return {
161
+ category,
162
+ allocated,
163
+ used,
164
+ remaining: Math.max(0, allocated - used),
165
+ }
166
+ }
167
+
168
+ /** Get allocation for a specific category */
169
+ getAllocationFor(category: BudgetCategory): number {
170
+ return this._allocations.get(category) ?? 0
171
+ }
172
+
173
+ /** Get total remaining input budget across all categories */
174
+ get totalRemaining(): number {
175
+ let totalUsed = 0
176
+ for (const v of this._used.values()) {
177
+ totalUsed += v
178
+ }
179
+ return Math.max(0, this._inputBudget - totalUsed)
180
+ }
181
+
182
+ /** Check if total usage exceeds input budget */
183
+ get isOverBudget(): boolean {
184
+ let totalUsed = 0
185
+ for (const v of this._used.values()) {
186
+ totalUsed += v
187
+ }
188
+ return totalUsed > this._inputBudget
189
+ }
190
+
191
+ /** Context window size */
192
+ get contextWindow(): number {
193
+ return this._contextWindow
194
+ }
195
+
196
+ /** Total input budget */
197
+ get inputBudget(): number {
198
+ return this._inputBudget
199
+ }
200
+
201
+ /** Output token reserve */
202
+ get outputReserve(): number {
203
+ return this._outputReserve
204
+ }
205
+ }
206
+
207
+ // =============================================================================
208
+ // Helpers
209
+ // =============================================================================
210
+
211
+ /** Get context window size for a model identifier */
212
+ export function getContextWindow(model?: string): number {
213
+ if (!model) return MODEL_CONTEXT_WINDOWS.default
214
+ return MODEL_CONTEXT_WINDOWS[model] ?? MODEL_CONTEXT_WINDOWS.default
215
+ }
216
+
217
+ /** Calculate input budget for a model */
218
+ export function calculateInputBudget(model?: string): number {
219
+ return Math.floor(getContextWindow(model) * INPUT_RATIO)
220
+ }
221
+
222
+ /** Calculate output reserve for a model */
223
+ export function calculateOutputReserve(model?: string): number {
224
+ const contextWindow = getContextWindow(model)
225
+ return contextWindow - Math.floor(contextWindow * INPUT_RATIO)
226
+ }
@@ -12,6 +12,7 @@ import commandInstaller from '../infrastructure/command-installer'
12
12
  import { formatCost } from '../schemas/metrics'
13
13
  import { createStalenessChecker, memoryService, syncService } from '../services'
14
14
  import { formatDiffPreview, formatFullDiff, generateSyncDiff } from '../services/diff-generator'
15
+ import { analysisStorage } from '../storage/analysis-storage'
15
16
  import { metricsStorage } from '../storage/metrics-storage'
16
17
  import type { AnalyzeOptions, CommandResult, ProjectContext } from '../types'
17
18
  import { getErrorMessage } from '../types/fs'
@@ -805,6 +806,9 @@ export class AnalysisCommands extends PrjctCommandsBase {
805
806
  // Get session info
806
807
  const sessionInfo = await checker.getSessionInfo(projectId)
807
808
 
809
+ // Get analysis status (PRJ-263)
810
+ const analysisStatus = await analysisStorage.getStatus(projectId)
811
+
808
812
  // JSON output mode
809
813
  if (options.json) {
810
814
  console.log(
@@ -812,9 +816,13 @@ export class AnalysisCommands extends PrjctCommandsBase {
812
816
  success: true,
813
817
  ...status,
814
818
  session: sessionInfo,
819
+ analysis: analysisStatus,
815
820
  })
816
821
  )
817
- return { success: true, data: { ...status, session: sessionInfo } }
822
+ return {
823
+ success: true,
824
+ data: { ...status, session: sessionInfo, analysis: analysisStatus },
825
+ }
818
826
  }
819
827
 
820
828
  // Human-readable output
@@ -822,9 +830,22 @@ export class AnalysisCommands extends PrjctCommandsBase {
822
830
  console.log(checker.formatStatus(status))
823
831
  console.log('')
824
832
  console.log(checker.formatSessionInfo(sessionInfo))
833
+
834
+ // Show analysis status (PRJ-263)
835
+ if (analysisStatus.hasSealed || analysisStatus.hasDraft) {
836
+ console.log('')
837
+ console.log('Analysis:')
838
+ if (analysisStatus.hasSealed) {
839
+ console.log(` Sealed: ${analysisStatus.sealedCommit} (${analysisStatus.sealedAt})`)
840
+ }
841
+ if (analysisStatus.hasDraft) {
842
+ console.log(` Draft: ${analysisStatus.draftCommit} (pending seal)`)
843
+ }
844
+ }
845
+
825
846
  console.log('')
826
847
 
827
- return { success: true, data: { ...status, session: sessionInfo } }
848
+ return { success: true, data: { ...status, session: sessionInfo, analysis: analysisStatus } }
828
849
  } catch (error) {
829
850
  const errMsg = getErrorMessage(error)
830
851
  if (options.json) {
@@ -836,6 +857,100 @@ export class AnalysisCommands extends PrjctCommandsBase {
836
857
  }
837
858
  }
838
859
 
860
+ /**
861
+ * prjct seal - Seal the current draft analysis (PRJ-263)
862
+ *
863
+ * Locks the current draft with a SHA-256 signature.
864
+ * Only sealed analysis feeds task context.
865
+ */
866
+ async seal(
867
+ projectPath: string = process.cwd(),
868
+ options: { json?: boolean } = {}
869
+ ): Promise<CommandResult> {
870
+ try {
871
+ const initResult = await this.ensureProjectInit(projectPath)
872
+ if (!initResult.success) return initResult
873
+
874
+ const projectId = await configManager.getProjectId(projectPath)
875
+ if (!projectId) {
876
+ if (options.json) {
877
+ console.log(JSON.stringify({ success: false, error: 'No project ID found' }))
878
+ }
879
+ return { success: false, error: 'No project ID found' }
880
+ }
881
+
882
+ const result = await analysisStorage.seal(projectId)
883
+
884
+ if (options.json) {
885
+ console.log(
886
+ JSON.stringify({
887
+ success: result.success,
888
+ signature: result.signature,
889
+ error: result.error,
890
+ })
891
+ )
892
+ return { success: result.success, error: result.error }
893
+ }
894
+
895
+ if (!result.success) {
896
+ out.fail(result.error || 'Seal failed')
897
+ return { success: false, error: result.error }
898
+ }
899
+
900
+ out.done('Analysis sealed')
901
+ console.log(` Signature: ${result.signature?.substring(0, 16)}...`)
902
+ console.log('')
903
+
904
+ return { success: true, data: { signature: result.signature } }
905
+ } catch (error) {
906
+ const errMsg = getErrorMessage(error)
907
+ if (options.json) {
908
+ console.log(JSON.stringify({ success: false, error: errMsg }))
909
+ } else {
910
+ out.fail(errMsg)
911
+ }
912
+ return { success: false, error: errMsg }
913
+ }
914
+ }
915
+
916
+ /**
917
+ * prjct verify - Verify integrity of sealed analysis (PRJ-263)
918
+ */
919
+ async verify(
920
+ projectPath: string = process.cwd(),
921
+ options: { json?: boolean } = {}
922
+ ): Promise<CommandResult> {
923
+ try {
924
+ const initResult = await this.ensureProjectInit(projectPath)
925
+ if (!initResult.success) return initResult
926
+
927
+ const projectId = await configManager.getProjectId(projectPath)
928
+ if (!projectId) {
929
+ return { success: false, error: 'No project ID found' }
930
+ }
931
+
932
+ const result = await analysisStorage.verify(projectId)
933
+
934
+ if (options.json) {
935
+ console.log(JSON.stringify(result))
936
+ return { success: result.valid }
937
+ }
938
+
939
+ if (result.valid) {
940
+ out.done(result.message)
941
+ } else {
942
+ out.fail(result.message)
943
+ }
944
+ console.log('')
945
+
946
+ return { success: result.valid, data: result }
947
+ } catch (error) {
948
+ const errMsg = getErrorMessage(error)
949
+ out.fail(errMsg)
950
+ return { success: false, error: errMsg }
951
+ }
952
+ }
953
+
839
954
  /**
840
955
  * Get session activity stats from today's events
841
956
  * @see PRJ-89
@@ -218,6 +218,35 @@ export const COMMANDS: CommandMeta[] = [
218
218
  'Configurable staleness thresholds',
219
219
  ],
220
220
  },
221
+ {
222
+ name: 'seal',
223
+ group: 'core',
224
+ description: 'Seal the current draft analysis with a commit-hash signature',
225
+ usage: { claude: '/p:seal', terminal: 'prjct seal' },
226
+ params: '[--json]',
227
+ implemented: true,
228
+ hasTemplate: false,
229
+ requiresProject: true,
230
+ features: [
231
+ 'Locks draft analysis with SHA-256 signature',
232
+ 'Only sealed analysis feeds task context',
233
+ 'Detects staleness when HEAD moves past sealed commit',
234
+ ],
235
+ },
236
+ {
237
+ name: 'verify',
238
+ group: 'core',
239
+ description: 'Verify integrity of sealed analysis',
240
+ usage: { claude: '/p:verify', terminal: 'prjct verify' },
241
+ params: '[--json]',
242
+ implemented: true,
243
+ hasTemplate: false,
244
+ requiresProject: true,
245
+ features: [
246
+ 'Recomputes SHA-256 signature and compares',
247
+ 'Detects if sealed analysis was modified',
248
+ ],
249
+ },
221
250
  {
222
251
  name: 'help',
223
252
  group: 'core',
@@ -218,6 +218,20 @@ class PrjctCommands {
218
218
  return this.analysis.status(projectPath, options)
219
219
  }
220
220
 
221
+ async seal(
222
+ projectPath: string = process.cwd(),
223
+ options: { json?: boolean } = {}
224
+ ): Promise<CommandResult> {
225
+ return this.analysis.seal(projectPath, options)
226
+ }
227
+
228
+ async verify(
229
+ projectPath: string = process.cwd(),
230
+ options: { json?: boolean } = {}
231
+ ): Promise<CommandResult> {
232
+ return this.analysis.verify(projectPath, options)
233
+ }
234
+
221
235
  // ========== Context Commands ==========
222
236
 
223
237
  async context(
@@ -91,6 +91,8 @@ export function registerAllCommands(): void {
91
91
  commandRegistry.registerMethod('sync', analysis, 'sync', getMeta('sync'))
92
92
  commandRegistry.registerMethod('stats', analysis, 'stats', getMeta('stats'))
93
93
  commandRegistry.registerMethod('status', analysis, 'status', getMeta('status'))
94
+ commandRegistry.registerMethod('seal', analysis, 'seal', getMeta('seal'))
95
+ commandRegistry.registerMethod('verify', analysis, 'verify', getMeta('verify'))
94
96
 
95
97
  // Setup commands
96
98
  commandRegistry.registerMethod('start', setup, 'start', getMeta('start'))
package/core/index.ts CHANGED
@@ -170,6 +170,8 @@ async function main(): Promise<void> {
170
170
  json: options.json === true,
171
171
  package: options.package ? String(options.package) : undefined,
172
172
  }),
173
+ seal: () => commands.seal(process.cwd(), { json: options.json === true }),
174
+ verify: () => commands.verify(process.cwd(), { json: options.json === true }),
173
175
  start: () => commands.start(),
174
176
  // Context (for Claude templates)
175
177
  context: (p) => commands.context(p),
@@ -2,37 +2,80 @@
2
2
  * Analysis Schema
3
3
  *
4
4
  * Defines the structure for analysis.json - repository analysis.
5
+ * Supports a 3-state lifecycle: DRAFT → VERIFIED → SEALED (PRJ-263).
5
6
  */
6
7
 
7
- import type { ModelMetadata } from './model'
8
+ import { z } from 'zod'
9
+ import { ModelMetadataSchema } from './model'
8
10
 
9
- export interface CodePattern {
10
- name: string
11
- description: string
12
- location?: string
13
- }
11
+ // =============================================================================
12
+ // Zod Schemas - Source of Truth
13
+ // =============================================================================
14
14
 
15
- export interface AntiPattern {
16
- issue: string
17
- file: string
18
- suggestion: string
19
- }
15
+ export const AnalysisStatusSchema = z.enum(['draft', 'verified', 'sealed'])
16
+
17
+ export const CodePatternSchema = z.object({
18
+ name: z.string(),
19
+ description: z.string(),
20
+ location: z.string().optional(),
21
+ })
20
22
 
21
- export interface AnalysisSchema {
22
- projectId: string
23
- languages: string[]
24
- frameworks: string[]
25
- packageManager?: string
26
- sourceDir?: string
27
- testDir?: string
28
- configFiles: string[]
29
- fileCount: number
30
- patterns: CodePattern[]
31
- antiPatterns: AntiPattern[]
32
- analyzedAt: string // ISO8601
23
+ export const AntiPatternSchema = z.object({
24
+ issue: z.string(),
25
+ file: z.string(),
26
+ suggestion: z.string(),
27
+ })
28
+
29
+ export const AnalysisItemSchema = z.object({
30
+ projectId: z.string(),
31
+ languages: z.array(z.string()),
32
+ frameworks: z.array(z.string()),
33
+ packageManager: z.string().optional(),
34
+ sourceDir: z.string().optional(),
35
+ testDir: z.string().optional(),
36
+ configFiles: z.array(z.string()),
37
+ fileCount: z.number(),
38
+ patterns: z.array(CodePatternSchema),
39
+ antiPatterns: z.array(AntiPatternSchema),
40
+ analyzedAt: z.string(), // ISO8601
33
41
  /** Which AI model was used for this analysis (PRJ-265) */
34
- modelMetadata?: ModelMetadata
35
- }
42
+ modelMetadata: ModelMetadataSchema.optional(),
43
+
44
+ // Sealable analysis fields (PRJ-263)
45
+ /** Lifecycle status: draft (regenerable), verified (confirmed correct), sealed (locked) */
46
+ status: AnalysisStatusSchema.default('draft'),
47
+ /** Git commit hash at the time of analysis */
48
+ commitHash: z.string().optional(),
49
+ /** SHA-256 signature of analysis content + commit hash */
50
+ signature: z.string().optional(),
51
+ /** When the analysis was sealed */
52
+ sealedAt: z.string().optional(), // ISO8601
53
+ /** When the analysis was verified */
54
+ verifiedAt: z.string().optional(), // ISO8601
55
+ })
56
+
57
+ // =============================================================================
58
+ // Inferred Types - Backward Compatible
59
+ // =============================================================================
60
+
61
+ export type AnalysisStatus = z.infer<typeof AnalysisStatusSchema>
62
+ export type CodePattern = z.infer<typeof CodePatternSchema>
63
+ export type AntiPattern = z.infer<typeof AntiPatternSchema>
64
+ /** Use z.input so optional fields with defaults (like status) remain optional in creation */
65
+ export type AnalysisSchema = z.input<typeof AnalysisItemSchema>
66
+
67
+ // =============================================================================
68
+ // Validation Helpers
69
+ // =============================================================================
70
+
71
+ /** Parse and validate analysis.json content */
72
+ export const parseAnalysis = (data: unknown): z.infer<typeof AnalysisItemSchema> =>
73
+ AnalysisItemSchema.parse(data)
74
+ export const safeParseAnalysis = (data: unknown) => AnalysisItemSchema.safeParse(data)
75
+
76
+ // =============================================================================
77
+ // Defaults
78
+ // =============================================================================
36
79
 
37
80
  export const DEFAULT_ANALYSIS: Omit<AnalysisSchema, 'projectId'> = {
38
81
  languages: [],
@@ -42,4 +85,5 @@ export const DEFAULT_ANALYSIS: Omit<AnalysisSchema, 'projectId'> = {
42
85
  patterns: [],
43
86
  antiPatterns: [],
44
87
  analyzedAt: new Date().toISOString(),
88
+ status: 'draft',
45
89
  }
@@ -29,14 +29,20 @@ export interface SelectedContext {
29
29
  }
30
30
  }
31
31
 
32
- /** Default token budget for context selection (increased for 200K+ context models) */
32
+ /**
33
+ * Default token budget for context selection.
34
+ * When a TokenBudgetCoordinator is available, this is overridden
35
+ * by the coordinator's file allocation.
36
+ *
37
+ * @see PRJ-266
38
+ */
33
39
  const DEFAULT_TOKEN_BUDGET = 80_000
34
40
 
35
41
  export interface ContextSelectionOptions {
36
42
  maxFiles?: number // Max files to return (default: 50)
37
43
  minScore?: number // Min relevance score (default: 30)
38
44
  includeGeneral?: boolean // Include 'general' domain files (default: true)
39
- tokenBudget?: number // Max estimated tokens (default: 80000)
45
+ tokenBudget?: number // Max estimated tokens (default: 80000, or from coordinator)
40
46
  }
41
47
 
42
48
  // ============================================================================
@@ -31,6 +31,7 @@ import { getErrorMessage } from '../errors'
31
31
  import commandInstaller from '../infrastructure/command-installer'
32
32
  import configManager from '../infrastructure/config-manager'
33
33
  import pathManager from '../infrastructure/path-manager'
34
+ import { analysisStorage } from '../storage/analysis-storage'
34
35
  import { metricsStorage } from '../storage/metrics-storage'
35
36
  import type {
36
37
  GitData,
@@ -176,6 +177,7 @@ class SyncService {
176
177
  this.updateProjectJson(git, stats),
177
178
  this.updateStateJson(stats, stack),
178
179
  this.logToMemory(git, stats),
180
+ this.saveDraftAnalysis(git, stats, stack),
179
181
  ])
180
182
 
181
183
  // 9. Record metrics for value dashboard
@@ -1102,6 +1104,39 @@ You are the ${name} expert for this project. Apply best practices for the detect
1102
1104
  }
1103
1105
  }
1104
1106
 
1107
+ // ==========================================================================
1108
+ // DRAFT ANALYSIS (PRJ-263)
1109
+ // ==========================================================================
1110
+
1111
+ /**
1112
+ * Save sync results as a draft analysis.
1113
+ * Preserves existing sealed analysis — only the draft is overwritten.
1114
+ */
1115
+ private async saveDraftAnalysis(
1116
+ git: GitData,
1117
+ stats: ProjectStats,
1118
+ _stack: StackDetection
1119
+ ): Promise<void> {
1120
+ try {
1121
+ const commitHash = git.recentCommits[0]?.hash || null
1122
+
1123
+ await analysisStorage.saveDraft(this.projectId!, {
1124
+ projectId: this.projectId!,
1125
+ languages: stats.languages,
1126
+ frameworks: stats.frameworks,
1127
+ configFiles: [],
1128
+ fileCount: stats.fileCount,
1129
+ patterns: [],
1130
+ antiPatterns: [],
1131
+ analyzedAt: dateHelper.getTimestamp(),
1132
+ status: 'draft',
1133
+ commitHash: commitHash ?? undefined,
1134
+ })
1135
+ } catch (error) {
1136
+ log.debug('Failed to save draft analysis (non-critical)', { error: getErrorMessage(error) })
1137
+ }
1138
+ }
1139
+
1105
1140
  // ==========================================================================
1106
1141
  // HELPERS
1107
1142
  // ==========================================================================