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.
- package/CHANGELOG.md +114 -1
- package/core/__tests__/agentic/prompt-assembly.test.ts +298 -0
- package/core/__tests__/agentic/prompt-builder.test.ts +2 -2
- package/core/__tests__/agentic/token-budget.test.ts +294 -0
- package/core/__tests__/storage/analysis-storage.test.ts +277 -0
- package/core/agentic/anti-hallucination.ts +124 -0
- package/core/agentic/environment-block.ts +102 -0
- package/core/agentic/injection-validator.ts +16 -0
- package/core/agentic/prompt-builder.ts +339 -167
- package/core/agentic/token-budget.ts +226 -0
- package/core/commands/analysis.ts +117 -2
- package/core/commands/command-data.ts +29 -0
- package/core/commands/commands.ts +14 -0
- package/core/commands/register.ts +2 -0
- package/core/index.ts +2 -0
- package/core/schemas/analysis.ts +69 -25
- package/core/services/context-selector.ts +8 -2
- package/core/services/sync-service.ts +35 -0
- package/core/storage/analysis-storage.ts +328 -0
- package/core/storage/index.ts +2 -0
- package/dist/bin/prjct.mjs +1357 -664
- package/package.json +1 -1
|
@@ -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 {
|
|
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),
|
package/core/schemas/analysis.ts
CHANGED
|
@@ -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
|
|
8
|
+
import { z } from 'zod'
|
|
9
|
+
import { ModelMetadataSchema } from './model'
|
|
8
10
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
location?: string
|
|
13
|
-
}
|
|
11
|
+
// =============================================================================
|
|
12
|
+
// Zod Schemas - Source of Truth
|
|
13
|
+
// =============================================================================
|
|
14
14
|
|
|
15
|
-
export
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
|
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
|
-
/**
|
|
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
|
// ==========================================================================
|