prjct-cli 1.10.0 → 1.12.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,277 @@
1
+ /**
2
+ * Analysis Storage Tests (PRJ-263)
3
+ *
4
+ * Tests for sealable analysis lifecycle:
5
+ * - Schema validation (draft/verified/sealed)
6
+ * - Signature computation and verification
7
+ * - Staleness detection
8
+ * - Draft preservation on re-sync
9
+ * - Backward compatibility
10
+ */
11
+
12
+ import { describe, expect, it } from 'bun:test'
13
+ import {
14
+ AnalysisItemSchema,
15
+ AnalysisStatusSchema,
16
+ parseAnalysis,
17
+ safeParseAnalysis,
18
+ } from '../../schemas/analysis'
19
+
20
+ // =============================================================================
21
+ // Schema Validation
22
+ // =============================================================================
23
+
24
+ describe('AnalysisStatusSchema', () => {
25
+ it('should accept valid statuses', () => {
26
+ expect(AnalysisStatusSchema.parse('draft')).toBe('draft')
27
+ expect(AnalysisStatusSchema.parse('verified')).toBe('verified')
28
+ expect(AnalysisStatusSchema.parse('sealed')).toBe('sealed')
29
+ })
30
+
31
+ it('should reject invalid statuses', () => {
32
+ expect(() => AnalysisStatusSchema.parse('pending')).toThrow()
33
+ expect(() => AnalysisStatusSchema.parse('active')).toThrow()
34
+ expect(() => AnalysisStatusSchema.parse('')).toThrow()
35
+ })
36
+ })
37
+
38
+ describe('AnalysisItemSchema', () => {
39
+ const validDraft = {
40
+ projectId: 'test-project-123',
41
+ languages: ['TypeScript'],
42
+ frameworks: ['Hono'],
43
+ configFiles: ['tsconfig.json'],
44
+ fileCount: 295,
45
+ patterns: [{ name: 'Service pattern', description: 'Classes with dependency injection' }],
46
+ antiPatterns: [],
47
+ analyzedAt: '2026-02-08T20:00:00.000Z',
48
+ status: 'draft' as const,
49
+ commitHash: 'abc1234',
50
+ }
51
+
52
+ it('should parse a valid draft analysis', () => {
53
+ const result = AnalysisItemSchema.parse(validDraft)
54
+ expect(result.status).toBe('draft')
55
+ expect(result.commitHash).toBe('abc1234')
56
+ expect(result.projectId).toBe('test-project-123')
57
+ })
58
+
59
+ it('should default status to draft when not provided', () => {
60
+ const { status, ...withoutStatus } = validDraft
61
+ const result = AnalysisItemSchema.parse(withoutStatus)
62
+ expect(result.status).toBe('draft')
63
+ })
64
+
65
+ it('should parse a sealed analysis with signature', () => {
66
+ const sealed = {
67
+ ...validDraft,
68
+ status: 'sealed' as const,
69
+ signature: 'sha256-abc123def456',
70
+ sealedAt: '2026-02-08T21:00:00.000Z',
71
+ }
72
+ const result = AnalysisItemSchema.parse(sealed)
73
+ expect(result.status).toBe('sealed')
74
+ expect(result.signature).toBe('sha256-abc123def456')
75
+ expect(result.sealedAt).toBe('2026-02-08T21:00:00.000Z')
76
+ })
77
+
78
+ it('should accept optional fields as undefined', () => {
79
+ const minimal = {
80
+ projectId: 'test',
81
+ languages: [],
82
+ frameworks: [],
83
+ configFiles: [],
84
+ fileCount: 0,
85
+ patterns: [],
86
+ antiPatterns: [],
87
+ analyzedAt: '2026-02-08T20:00:00.000Z',
88
+ }
89
+ const result = AnalysisItemSchema.parse(minimal)
90
+ expect(result.status).toBe('draft')
91
+ expect(result.commitHash).toBeUndefined()
92
+ expect(result.signature).toBeUndefined()
93
+ expect(result.sealedAt).toBeUndefined()
94
+ expect(result.modelMetadata).toBeUndefined()
95
+ })
96
+
97
+ it('should reject missing required fields', () => {
98
+ expect(() => AnalysisItemSchema.parse({})).toThrow()
99
+ expect(() => AnalysisItemSchema.parse({ projectId: 'test' })).toThrow()
100
+ })
101
+ })
102
+
103
+ describe('parseAnalysis / safeParseAnalysis', () => {
104
+ it('should parse valid data', () => {
105
+ const data = {
106
+ projectId: 'test',
107
+ languages: ['TypeScript'],
108
+ frameworks: [],
109
+ configFiles: [],
110
+ fileCount: 10,
111
+ patterns: [],
112
+ antiPatterns: [],
113
+ analyzedAt: '2026-02-08T20:00:00.000Z',
114
+ }
115
+ const result = parseAnalysis(data)
116
+ expect(result.projectId).toBe('test')
117
+ expect(result.status).toBe('draft')
118
+ })
119
+
120
+ it('should return success for safeParseAnalysis with valid data', () => {
121
+ const data = {
122
+ projectId: 'test',
123
+ languages: [],
124
+ frameworks: [],
125
+ configFiles: [],
126
+ fileCount: 0,
127
+ patterns: [],
128
+ antiPatterns: [],
129
+ analyzedAt: '2026-02-08T20:00:00.000Z',
130
+ }
131
+ const result = safeParseAnalysis(data)
132
+ expect(result.success).toBe(true)
133
+ })
134
+
135
+ it('should return failure for safeParseAnalysis with invalid data', () => {
136
+ const result = safeParseAnalysis({ invalid: true })
137
+ expect(result.success).toBe(false)
138
+ })
139
+ })
140
+
141
+ // =============================================================================
142
+ // Staleness Detection (pure function tests)
143
+ // =============================================================================
144
+
145
+ describe('staleness detection', () => {
146
+ // Test checkStaleness method from AnalysisStorage
147
+ it('should detect stale analysis when commits differ', () => {
148
+ const { analysisStorage } = require('../../storage/analysis-storage')
149
+ const result = analysisStorage.checkStaleness('abc1234', 'def5678')
150
+ expect(result.isStale).toBe(true)
151
+ expect(result.sealedCommit).toBe('abc1234')
152
+ expect(result.currentCommit).toBe('def5678')
153
+ })
154
+
155
+ it('should detect fresh analysis when commits match', () => {
156
+ const { analysisStorage } = require('../../storage/analysis-storage')
157
+ const result = analysisStorage.checkStaleness('abc1234', 'abc1234')
158
+ expect(result.isStale).toBe(false)
159
+ })
160
+
161
+ it('should handle null sealed commit', () => {
162
+ const { analysisStorage } = require('../../storage/analysis-storage')
163
+ const result = analysisStorage.checkStaleness(null, 'abc1234')
164
+ expect(result.isStale).toBe(false)
165
+ expect(result.message).toContain('No sealed analysis')
166
+ })
167
+
168
+ it('should handle null current commit', () => {
169
+ const { analysisStorage } = require('../../storage/analysis-storage')
170
+ const result = analysisStorage.checkStaleness('abc1234', null)
171
+ expect(result.isStale).toBe(true)
172
+ expect(result.message).toContain('Cannot determine')
173
+ })
174
+ })
175
+
176
+ // =============================================================================
177
+ // Signature Computation (determinism test)
178
+ // =============================================================================
179
+
180
+ describe('signature computation', () => {
181
+ it('should produce deterministic signatures for same input', () => {
182
+ const { createHash } = require('node:crypto')
183
+
184
+ const analysis = {
185
+ projectId: 'test',
186
+ languages: ['TypeScript'],
187
+ frameworks: ['Hono'],
188
+ configFiles: [],
189
+ fileCount: 100,
190
+ patterns: [],
191
+ antiPatterns: [],
192
+ analyzedAt: '2026-02-08T20:00:00.000Z',
193
+ commitHash: 'abc1234',
194
+ }
195
+
196
+ const canonical = {
197
+ projectId: analysis.projectId,
198
+ languages: analysis.languages,
199
+ frameworks: analysis.frameworks,
200
+ packageManager: undefined,
201
+ sourceDir: undefined,
202
+ testDir: undefined,
203
+ configFiles: analysis.configFiles,
204
+ fileCount: analysis.fileCount,
205
+ patterns: analysis.patterns,
206
+ antiPatterns: analysis.antiPatterns,
207
+ analyzedAt: analysis.analyzedAt,
208
+ commitHash: analysis.commitHash,
209
+ }
210
+
211
+ const sig1 = createHash('sha256').update(JSON.stringify(canonical)).digest('hex')
212
+ const sig2 = createHash('sha256').update(JSON.stringify(canonical)).digest('hex')
213
+
214
+ expect(sig1).toBe(sig2)
215
+ expect(sig1).toHaveLength(64) // SHA-256 hex = 64 chars
216
+ })
217
+
218
+ it('should produce different signatures for different inputs', () => {
219
+ const { createHash } = require('node:crypto')
220
+
221
+ const data1 = JSON.stringify({ projectId: 'a', fileCount: 1 })
222
+ const data2 = JSON.stringify({ projectId: 'b', fileCount: 2 })
223
+
224
+ const sig1 = createHash('sha256').update(data1).digest('hex')
225
+ const sig2 = createHash('sha256').update(data2).digest('hex')
226
+
227
+ expect(sig1).not.toBe(sig2)
228
+ })
229
+ })
230
+
231
+ // =============================================================================
232
+ // Backward Compatibility
233
+ // =============================================================================
234
+
235
+ describe('backward compatibility', () => {
236
+ it('should parse old analysis.json without seal fields', () => {
237
+ const oldFormat = {
238
+ projectId: 'old-project',
239
+ languages: ['JavaScript'],
240
+ frameworks: ['Express'],
241
+ configFiles: ['package.json'],
242
+ fileCount: 50,
243
+ patterns: [],
244
+ antiPatterns: [],
245
+ analyzedAt: '2025-12-01T00:00:00.000Z',
246
+ // No status, commitHash, signature, sealedAt
247
+ }
248
+
249
+ const result = AnalysisItemSchema.parse(oldFormat)
250
+ expect(result.status).toBe('draft') // Default
251
+ expect(result.commitHash).toBeUndefined()
252
+ expect(result.signature).toBeUndefined()
253
+ expect(result.sealedAt).toBeUndefined()
254
+ })
255
+
256
+ it('should parse old analysis with modelMetadata but no seal fields', () => {
257
+ const oldWithModel = {
258
+ projectId: 'old-project',
259
+ languages: ['TypeScript'],
260
+ frameworks: [],
261
+ configFiles: [],
262
+ fileCount: 10,
263
+ patterns: [],
264
+ antiPatterns: [],
265
+ analyzedAt: '2026-01-15T00:00:00.000Z',
266
+ modelMetadata: {
267
+ provider: 'claude',
268
+ model: 'sonnet',
269
+ recordedAt: '2026-01-15T00:00:00.000Z',
270
+ },
271
+ }
272
+
273
+ const result = AnalysisItemSchema.parse(oldWithModel)
274
+ expect(result.status).toBe('draft')
275
+ expect(result.modelMetadata?.provider).toBe('claude')
276
+ })
277
+ })
@@ -0,0 +1,237 @@
1
+ /**
2
+ * Subtask Handoff Tests (PRJ-262)
3
+ *
4
+ * Tests for mandatory subtask output and handoff:
5
+ * - SubtaskCompletionDataSchema validation
6
+ * - SubtaskSummarySchema required fields
7
+ * - validateSubtaskCompletion helper
8
+ * - Backward compatibility with old state.json
9
+ */
10
+
11
+ import { describe, expect, it } from 'bun:test'
12
+ import {
13
+ SubtaskCompletionDataSchema,
14
+ SubtaskSchema,
15
+ SubtaskSummarySchema,
16
+ validateSubtaskCompletion,
17
+ } from '../../schemas/state'
18
+
19
+ // =============================================================================
20
+ // SubtaskSummarySchema — outputForNextAgent is now required
21
+ // =============================================================================
22
+
23
+ describe('SubtaskSummarySchema', () => {
24
+ const validSummary = {
25
+ title: 'Implement auth middleware',
26
+ description: 'Added JWT verification to all protected routes',
27
+ filesChanged: [
28
+ { path: 'src/middleware/auth.ts', action: 'created' as const },
29
+ { path: 'src/routes/api.ts', action: 'modified' as const },
30
+ ],
31
+ whatWasDone: ['Created JWT middleware', 'Applied to API routes'],
32
+ outputForNextAgent:
33
+ 'Auth middleware is in place. Next subtask should add role-based access control.',
34
+ }
35
+
36
+ it('should parse a valid summary with all required fields', () => {
37
+ const result = SubtaskSummarySchema.parse(validSummary)
38
+ expect(result.outputForNextAgent).toBe(validSummary.outputForNextAgent)
39
+ expect(result.whatWasDone).toHaveLength(2)
40
+ })
41
+
42
+ it('should reject missing outputForNextAgent', () => {
43
+ const { outputForNextAgent, ...withoutOutput } = validSummary
44
+ expect(() => SubtaskSummarySchema.parse(withoutOutput)).toThrow()
45
+ })
46
+
47
+ it('should reject empty outputForNextAgent', () => {
48
+ expect(() => SubtaskSummarySchema.parse({ ...validSummary, outputForNextAgent: '' })).toThrow()
49
+ })
50
+
51
+ it('should reject empty whatWasDone array', () => {
52
+ expect(() => SubtaskSummarySchema.parse({ ...validSummary, whatWasDone: [] })).toThrow()
53
+ })
54
+
55
+ it('should allow optional notes', () => {
56
+ const result = SubtaskSummarySchema.parse({ ...validSummary, notes: 'Watch for rate limits' })
57
+ expect(result.notes).toBe('Watch for rate limits')
58
+ })
59
+
60
+ it('should allow missing notes', () => {
61
+ const result = SubtaskSummarySchema.parse(validSummary)
62
+ expect(result.notes).toBeUndefined()
63
+ })
64
+ })
65
+
66
+ // =============================================================================
67
+ // SubtaskCompletionDataSchema — validates completion requirements
68
+ // =============================================================================
69
+
70
+ describe('SubtaskCompletionDataSchema', () => {
71
+ const validCompletion = {
72
+ output: 'Implemented auth middleware with JWT verification',
73
+ summary: {
74
+ title: 'Auth middleware',
75
+ description: 'JWT verification for protected routes',
76
+ filesChanged: [{ path: 'src/auth.ts', action: 'created' as const }],
77
+ whatWasDone: ['Created JWT middleware'],
78
+ outputForNextAgent: 'Middleware ready, add RBAC next.',
79
+ },
80
+ }
81
+
82
+ it('should parse valid completion data', () => {
83
+ const result = SubtaskCompletionDataSchema.parse(validCompletion)
84
+ expect(result.output).toBe(validCompletion.output)
85
+ expect(result.summary.outputForNextAgent).toBe('Middleware ready, add RBAC next.')
86
+ })
87
+
88
+ it('should reject missing output', () => {
89
+ const { output, ...withoutOutput } = validCompletion
90
+ expect(() => SubtaskCompletionDataSchema.parse(withoutOutput)).toThrow()
91
+ })
92
+
93
+ it('should reject empty output', () => {
94
+ expect(() => SubtaskCompletionDataSchema.parse({ ...validCompletion, output: '' })).toThrow()
95
+ })
96
+
97
+ it('should reject missing summary', () => {
98
+ expect(() => SubtaskCompletionDataSchema.parse({ output: 'some output' })).toThrow()
99
+ })
100
+
101
+ it('should reject summary without outputForNextAgent', () => {
102
+ const badSummary = { ...validCompletion.summary }
103
+ // @ts-expect-error - intentionally testing invalid data
104
+ delete badSummary.outputForNextAgent
105
+ expect(() => SubtaskCompletionDataSchema.parse({ output: 'ok', summary: badSummary })).toThrow()
106
+ })
107
+ })
108
+
109
+ // =============================================================================
110
+ // validateSubtaskCompletion helper
111
+ // =============================================================================
112
+
113
+ describe('validateSubtaskCompletion', () => {
114
+ const validData = {
115
+ output: 'Task done',
116
+ summary: {
117
+ title: 'Test',
118
+ description: 'Testing',
119
+ filesChanged: [],
120
+ whatWasDone: ['Did the thing'],
121
+ outputForNextAgent: 'Context for next.',
122
+ },
123
+ }
124
+
125
+ it('should return success for valid data', () => {
126
+ const result = validateSubtaskCompletion(validData)
127
+ expect(result.success).toBe(true)
128
+ if (result.success) {
129
+ expect(result.data.output).toBe('Task done')
130
+ }
131
+ })
132
+
133
+ it('should return errors for missing output', () => {
134
+ const result = validateSubtaskCompletion({ summary: validData.summary })
135
+ expect(result.success).toBe(false)
136
+ if (!result.success) {
137
+ expect(result.errors.length).toBeGreaterThan(0)
138
+ expect(result.errors.some((e) => e.includes('output'))).toBe(true)
139
+ }
140
+ })
141
+
142
+ it('should return errors for empty whatWasDone', () => {
143
+ const result = validateSubtaskCompletion({
144
+ output: 'ok',
145
+ summary: { ...validData.summary, whatWasDone: [] },
146
+ })
147
+ expect(result.success).toBe(false)
148
+ if (!result.success) {
149
+ expect(result.errors.some((e) => e.includes('whatWasDone'))).toBe(true)
150
+ }
151
+ })
152
+
153
+ it('should return errors for completely invalid data', () => {
154
+ const result = validateSubtaskCompletion({})
155
+ expect(result.success).toBe(false)
156
+ if (!result.success) {
157
+ expect(result.errors.length).toBeGreaterThan(0)
158
+ }
159
+ })
160
+ })
161
+
162
+ // =============================================================================
163
+ // Backward Compatibility
164
+ // =============================================================================
165
+
166
+ describe('backward compatibility', () => {
167
+ it('should parse old SubtaskSchema without summary or output', () => {
168
+ const oldSubtask = {
169
+ id: 'subtask-1',
170
+ description: 'Old subtask without handoff',
171
+ domain: 'backend',
172
+ agent: 'backend.md',
173
+ status: 'completed' as const,
174
+ dependsOn: [],
175
+ startedAt: '2026-01-01T00:00:00.000Z',
176
+ completedAt: '2026-01-01T01:00:00.000Z',
177
+ // No output, no summary — old format
178
+ }
179
+ const result = SubtaskSchema.parse(oldSubtask)
180
+ expect(result.output).toBeUndefined()
181
+ expect(result.summary).toBeUndefined()
182
+ expect(result.status).toBe('completed')
183
+ })
184
+
185
+ it('should parse pending subtask without completion fields', () => {
186
+ const pending = {
187
+ id: 'subtask-2',
188
+ description: 'Pending subtask',
189
+ domain: 'frontend',
190
+ agent: 'frontend.md',
191
+ status: 'pending' as const,
192
+ dependsOn: ['subtask-1'],
193
+ }
194
+ const result = SubtaskSchema.parse(pending)
195
+ expect(result.status).toBe('pending')
196
+ expect(result.output).toBeUndefined()
197
+ expect(result.summary).toBeUndefined()
198
+ })
199
+
200
+ it('should parse subtask with output but no summary (transition format)', () => {
201
+ const transitional = {
202
+ id: 'subtask-1',
203
+ description: 'Transitional format',
204
+ domain: 'backend',
205
+ agent: 'backend.md',
206
+ status: 'completed' as const,
207
+ dependsOn: [],
208
+ output: 'Done with basic output',
209
+ // No summary yet
210
+ }
211
+ const result = SubtaskSchema.parse(transitional)
212
+ expect(result.output).toBe('Done with basic output')
213
+ expect(result.summary).toBeUndefined()
214
+ })
215
+
216
+ it('should parse subtask with full handoff data', () => {
217
+ const withHandoff = {
218
+ id: 'subtask-1',
219
+ description: 'Full handoff format',
220
+ domain: 'backend',
221
+ agent: 'backend.md',
222
+ status: 'completed' as const,
223
+ dependsOn: [],
224
+ output: 'Implemented the feature',
225
+ summary: {
226
+ title: 'Backend API',
227
+ description: 'Created REST endpoints',
228
+ filesChanged: [{ path: 'src/api.ts', action: 'created' as const }],
229
+ whatWasDone: ['Created endpoints', 'Added validation'],
230
+ outputForNextAgent: 'API is ready at /api/v1. Add auth next.',
231
+ },
232
+ }
233
+ const result = SubtaskSchema.parse(withHandoff)
234
+ expect(result.summary?.outputForNextAgent).toBe('API is ready at /api/v1. Add auth next.')
235
+ expect(result.summary?.whatWasDone).toHaveLength(2)
236
+ })
237
+ })
@@ -800,6 +800,24 @@ class PromptBuilder {
800
800
  if (currentSubtask.dependsOn.length > 0) {
801
801
  parts.push(`Dependencies: ${currentSubtask.dependsOn.join(', ')}\n`)
802
802
  }
803
+
804
+ // Inject previous subtask handoff for context continuity (PRJ-262)
805
+ if (currentSubtask.handoff) {
806
+ const h = currentSubtask.handoff
807
+ parts.push('\n### Previous Subtask Handoff\n\n')
808
+ parts.push(`**From:** ${h.fromSubtask}\n\n`)
809
+ parts.push('**What was done:**\n')
810
+ for (const item of h.whatWasDone) {
811
+ parts.push(`- ${item}\n`)
812
+ }
813
+ if (h.filesChanged.length > 0) {
814
+ parts.push('\n**Files changed:**\n')
815
+ for (const f of h.filesChanged) {
816
+ parts.push(`- \`${f.path}\` (${f.action})\n`)
817
+ }
818
+ }
819
+ parts.push(`\n**Context for this subtask:**\n${h.outputForNextAgent}\n`)
820
+ }
803
821
  }
804
822
  parts.push('\n')
805
823
  }
@@ -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