sdd-mcp-server 3.5.1 → 5.0.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/README.md +92 -683
- package/agents/architect.md +15 -93
- package/agents/implementer.md +16 -141
- package/agents/planner.md +16 -84
- package/agents/reviewer.md +16 -239
- package/agents/security-auditor.md +16 -114
- package/agents/tdd-guide.md +17 -228
- package/dist/adapters/cli/SDDToolAdapter.d.ts +14 -8
- package/dist/adapters/cli/SDDToolAdapter.js +188 -405
- package/dist/adapters/cli/SDDToolAdapter.js.map +1 -1
- package/dist/application/services/ContextCompactionService.d.ts +88 -16
- package/dist/application/services/ContextCompactionService.js +474 -187
- package/dist/application/services/ContextCompactionService.js.map +1 -1
- package/dist/application/services/ProjectService.js +3 -3
- package/dist/application/services/ProjectService.js.map +1 -1
- package/dist/application/services/SpecPathResolver.d.ts +24 -0
- package/dist/application/services/SpecPathResolver.js +70 -0
- package/dist/application/services/SpecPathResolver.js.map +1 -0
- package/dist/application/services/WorkflowEngineService.d.ts +214 -50
- package/dist/application/services/WorkflowEngineService.js +1447 -292
- package/dist/application/services/WorkflowEngineService.js.map +1 -1
- package/dist/application/services/WorkflowErrors.d.ts +16 -0
- package/dist/application/services/WorkflowErrors.js +53 -0
- package/dist/application/services/WorkflowErrors.js.map +1 -0
- package/dist/application/services/WorkflowValidationService.d.ts +25 -46
- package/dist/application/services/WorkflowValidationService.js +284 -627
- package/dist/application/services/WorkflowValidationService.js.map +1 -1
- package/dist/cli/install-skills.d.ts +3 -9
- package/dist/cli/install-skills.js +129 -174
- package/dist/cli/install-skills.js.map +1 -1
- package/dist/cli/install-target.d.ts +42 -8
- package/dist/cli/install-target.js +27 -9
- package/dist/cli/install-target.js.map +1 -1
- package/dist/cli/sdd-mcp-cli.d.ts +1 -1
- package/dist/cli/sdd-mcp-cli.js +7 -6
- package/dist/cli/sdd-mcp-cli.js.map +1 -1
- package/dist/cli/tool-support/claude-code.js +17 -34
- package/dist/cli/tool-support/claude-code.js.map +1 -1
- package/dist/cli/tool-support/codex.d.ts +0 -53
- package/dist/cli/tool-support/codex.js +10 -94
- package/dist/cli/tool-support/codex.js.map +1 -1
- package/dist/cli/tool-support/index.d.ts +3 -2
- package/dist/cli/tool-support/index.js +3 -1
- package/dist/cli/tool-support/index.js.map +1 -1
- package/dist/cli/tool-support/mcp-registration.d.ts +22 -0
- package/dist/cli/tool-support/mcp-registration.js +275 -0
- package/dist/cli/tool-support/mcp-registration.js.map +1 -0
- package/dist/cli/tool-support/omp.d.ts +5 -0
- package/dist/cli/tool-support/omp.js +47 -0
- package/dist/cli/tool-support/omp.js.map +1 -0
- package/dist/cli/tool-support/root-guidance.d.ts +2 -9
- package/dist/cli/tool-support/root-guidance.js +44 -37
- package/dist/cli/tool-support/root-guidance.js.map +1 -1
- package/dist/cli/tool-support/target-agent-renderer.d.ts +1 -0
- package/dist/cli/tool-support/target-agent-renderer.js +37 -4
- package/dist/cli/tool-support/target-agent-renderer.js.map +1 -1
- package/dist/cli/tool-support/target-installer.d.ts +9 -3
- package/dist/cli/tool-support/target-installer.js +100 -26
- package/dist/cli/tool-support/target-installer.js.map +1 -1
- package/dist/cli/utils/preserving-writer.d.ts +56 -0
- package/dist/cli/utils/preserving-writer.js +603 -10
- package/dist/cli/utils/preserving-writer.js.map +1 -1
- package/dist/domain/ports.d.ts +4 -0
- package/dist/domain/types.d.ts +52 -7
- package/dist/domain/types.js +5 -4
- package/dist/domain/types.js.map +1 -1
- package/dist/index.d.ts +13 -10
- package/dist/index.js +16 -1199
- package/dist/index.js.map +1 -1
- package/dist/infrastructure/adapters/NodeFileSystemAdapter.d.ts +3 -0
- package/dist/infrastructure/adapters/NodeFileSystemAdapter.js +10 -0
- package/dist/infrastructure/adapters/NodeFileSystemAdapter.js.map +1 -1
- package/dist/infrastructure/mcp/CapabilityNegotiator.js +3 -3
- package/dist/infrastructure/mcp/CapabilityNegotiator.js.map +1 -1
- package/dist/infrastructure/mcp/MCPServer.js +13 -13
- package/dist/infrastructure/mcp/MCPServer.js.map +1 -1
- package/dist/infrastructure/mcp/ToolRegistry.d.ts +5 -1
- package/dist/infrastructure/mcp/ToolRegistry.js +11 -4
- package/dist/infrastructure/mcp/ToolRegistry.js.map +1 -1
- package/dist/infrastructure/mcp/sddToolDefinitions.d.ts +6 -0
- package/dist/infrastructure/mcp/sddToolDefinitions.js +110 -0
- package/dist/infrastructure/mcp/sddToolDefinitions.js.map +1 -0
- package/dist/infrastructure/schemas/project.schema.d.ts +2 -2
- package/dist/infrastructure/schemas/project.schema.js +2 -2
- package/dist/infrastructure/schemas/project.schema.js.map +1 -1
- package/dist/shared/version.d.ts +3 -0
- package/dist/shared/version.js +4 -0
- package/dist/shared/version.js.map +1 -0
- package/dist/utils/atomicWrite.d.ts +8 -35
- package/dist/utils/atomicWrite.js +24 -57
- package/dist/utils/atomicWrite.js.map +1 -1
- package/dist/utils/withFilesystemLock.d.ts +22 -0
- package/dist/utils/withFilesystemLock.js +219 -0
- package/dist/utils/withFilesystemLock.js.map +1 -0
- package/mcp-server.js +5 -2883
- package/package.json +8 -3
- package/scripts/context-usage-report.mjs +602 -0
- package/sdd-entry.js +17 -6
- package/skills/sdd-commit/REFERENCE.md +31 -0
- package/skills/sdd-commit/SKILL.md +17 -273
- package/skills/sdd-design/REFERENCE.md +51 -0
- package/skills/sdd-design/SKILL.md +25 -262
- package/skills/sdd-implement/REFERENCE.md +30 -0
- package/skills/sdd-implement/SKILL.md +27 -284
- package/skills/sdd-requirements/REFERENCE.md +39 -0
- package/skills/sdd-requirements/SKILL.md +28 -132
- package/skills/sdd-review/REFERENCE.md +26 -0
- package/skills/sdd-review/SKILL.md +17 -181
- package/skills/sdd-security-check/REFERENCE.md +19 -0
- package/skills/sdd-security-check/SKILL.md +18 -184
- package/skills/sdd-steering/REFERENCE.md +25 -0
- package/skills/sdd-steering/SKILL.md +18 -216
- package/skills/sdd-steering-custom/REFERENCE.md +27 -0
- package/skills/sdd-steering-custom/SKILL.md +19 -203
- package/skills/sdd-tasks/REFERENCE.md +25 -0
- package/skills/sdd-tasks/SKILL.md +27 -244
- package/skills/sdd-test-gen/REFERENCE.md +15 -0
- package/skills/sdd-test-gen/SKILL.md +17 -287
- package/skills/simple-task/REFERENCE.md +22 -0
- package/skills/simple-task/SKILL.md +17 -138
- package/templates/CLAUDE.md +13 -31
- package/templates/codex-AGENTS.md +7 -9
- package/rules/git-workflow.md +0 -92
- package/rules/sdd-workflow.md +0 -116
|
@@ -10,9 +10,64 @@ var __metadata = (this && this.__metadata) || function (k, v) {
|
|
|
10
10
|
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
11
11
|
return function (target, key) { decorator(target, key, paramIndex); }
|
|
12
12
|
};
|
|
13
|
+
import { createHash } from 'node:crypto';
|
|
14
|
+
import path from 'node:path';
|
|
13
15
|
import { injectable, inject } from 'inversify';
|
|
14
|
-
import { v4 as uuidv4 } from 'uuid';
|
|
15
16
|
import { TYPES } from '../../infrastructure/di/types.js';
|
|
17
|
+
import { SpecPathResolver } from './SpecPathResolver.js';
|
|
18
|
+
export class ContextBudgetTooSmallError extends Error {
|
|
19
|
+
requested;
|
|
20
|
+
minimumRequired;
|
|
21
|
+
code = 'ContextBudgetTooSmall';
|
|
22
|
+
constructor(requested, minimumRequired) {
|
|
23
|
+
super(`Context budget ${requested} is below the mandatory minimum ${minimumRequired}`);
|
|
24
|
+
this.requested = requested;
|
|
25
|
+
this.minimumRequired = minimumRequired;
|
|
26
|
+
this.name = 'ContextBudgetTooSmallError';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export class ContextBudgetExceededError extends Error {
|
|
30
|
+
requested;
|
|
31
|
+
required;
|
|
32
|
+
code = 'ContextBudgetExceeded';
|
|
33
|
+
constructor(requested, required) {
|
|
34
|
+
super(`Full context requires ${required} estimated tokens; requested ${requested}`);
|
|
35
|
+
this.requested = requested;
|
|
36
|
+
this.required = required;
|
|
37
|
+
this.name = 'ContextBudgetExceededError';
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export class PhaseNotApprovedError extends Error {
|
|
41
|
+
phase;
|
|
42
|
+
code = 'PhaseNotApproved';
|
|
43
|
+
constructor(phase) {
|
|
44
|
+
super(`Phase is not approved: ${phase}`);
|
|
45
|
+
this.phase = phase;
|
|
46
|
+
this.name = 'PhaseNotApprovedError';
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export class ContextArtifactDriftError extends Error {
|
|
50
|
+
phase;
|
|
51
|
+
code = 'ArtifactDrift';
|
|
52
|
+
constructor(phase) {
|
|
53
|
+
super(`Approved artifact has drifted: ${phase}`);
|
|
54
|
+
this.phase = phase;
|
|
55
|
+
this.name = 'ContextArtifactDriftError';
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
export class ContextSourceError extends Error {
|
|
59
|
+
sourcePath;
|
|
60
|
+
code = 'ContextSourceError';
|
|
61
|
+
constructor(sourcePath, message) {
|
|
62
|
+
super(`${message}: ${sourcePath}`);
|
|
63
|
+
this.sourcePath = sourcePath;
|
|
64
|
+
this.name = 'ContextSourceError';
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const MODE_BUDGETS = { compact: 2048, standard: 4096, full: 16384 };
|
|
68
|
+
const PHASES = ['requirements', 'design', 'tasks'];
|
|
69
|
+
const HANDOFF_SCHEMA = 2;
|
|
70
|
+
const SELECTION_VERSION = 2;
|
|
16
71
|
let ContextCompactionService = class ContextCompactionService {
|
|
17
72
|
fileSystem;
|
|
18
73
|
logger;
|
|
@@ -20,227 +75,459 @@ let ContextCompactionService = class ContextCompactionService {
|
|
|
20
75
|
this.fileSystem = fileSystem;
|
|
21
76
|
this.logger = logger;
|
|
22
77
|
}
|
|
78
|
+
async loadContext(requestOrProject, legacyMode = 'compact') {
|
|
79
|
+
if (!('projectRoot' in requestOrProject)) {
|
|
80
|
+
const result = await this.loadContextRequest({ projectRoot: requestOrProject.path, featureName: requestOrProject.name, mode: legacyMode });
|
|
81
|
+
return result.content ?? '';
|
|
82
|
+
}
|
|
83
|
+
return this.loadContextRequest(requestOrProject);
|
|
84
|
+
}
|
|
23
85
|
async generatePhaseHandoff(project, approvedPhase) {
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
const handoffPath = `${contextDir}/handoff.md`;
|
|
27
|
-
const phaseHandoffPath = `${contextDir}/${approvedPhase}-handoff.md`;
|
|
28
|
-
this.logger.info('Generating compact workflow handoff', {
|
|
29
|
-
correlationId,
|
|
30
|
-
projectId: project.id,
|
|
31
|
-
approvedPhase
|
|
32
|
-
});
|
|
33
|
-
await this.fileSystem.mkdir(contextDir);
|
|
34
|
-
const documents = await this.loadAvailableDocuments(project);
|
|
35
|
-
const sourceCharacters = documents.reduce((sum, doc) => sum + doc.content.length, 0);
|
|
36
|
-
const content = this.buildHandoff(project, approvedPhase, documents, sourceCharacters);
|
|
37
|
-
await this.fileSystem.writeFile(handoffPath, content);
|
|
38
|
-
await this.fileSystem.writeFile(phaseHandoffPath, content);
|
|
39
|
-
const estimate = this.estimateContextSize(sourceCharacters, content.length);
|
|
40
|
-
this.logger.info('Compact workflow handoff generated', {
|
|
41
|
-
correlationId,
|
|
42
|
-
projectId: project.id,
|
|
43
|
-
approvedPhase,
|
|
44
|
-
sourceTokens: estimate.sourceTokens,
|
|
45
|
-
compactTokens: estimate.compactTokens,
|
|
46
|
-
reductionPercentage: estimate.reductionPercentage
|
|
47
|
-
});
|
|
86
|
+
const result = await this.loadContextRequest({ projectRoot: project.path, featureName: project.name, phase: approvedPhase, mode: 'compact' });
|
|
87
|
+
const content = result.content ?? '';
|
|
48
88
|
return {
|
|
49
|
-
path:
|
|
89
|
+
path: path.join(project.path, '.spec', 'specs', project.name, 'context', 'handoff.md'),
|
|
50
90
|
content,
|
|
51
|
-
|
|
91
|
+
fingerprint: result.fingerprint,
|
|
92
|
+
estimate: this.estimateContextSize(result.sourceCharacters, result.payloadCharacters),
|
|
52
93
|
};
|
|
53
94
|
}
|
|
54
|
-
async
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const
|
|
95
|
+
async invalidateCanonicalHandoff(request) {
|
|
96
|
+
const resolved = await new SpecPathResolver(this.fileSystem).resolve(request.projectRoot, request.featureName);
|
|
97
|
+
const handoffPath = path.join(resolved.featureRoot, 'context', 'handoff.md');
|
|
98
|
+
const contextRoot = path.dirname(handoffPath);
|
|
99
|
+
const resolver = new SpecPathResolver(this.fileSystem);
|
|
100
|
+
await resolver.assertContained(resolved.featureRoot, contextRoot);
|
|
101
|
+
await resolver.assertContained(resolved.featureRoot, handoffPath);
|
|
59
102
|
if (await this.fileSystem.exists(handoffPath)) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
return [
|
|
64
|
-
handoff,
|
|
65
|
-
spec ? '\n## Current Spec Metadata\n\n```json\n' + spec + '\n```' : ''
|
|
66
|
-
].filter(Boolean).join('\n');
|
|
67
|
-
}
|
|
68
|
-
return handoff;
|
|
103
|
+
if (!this.fileSystem.unlink)
|
|
104
|
+
throw new Error('FileSystemPort.unlink is required to invalidate context');
|
|
105
|
+
await this.fileSystem.unlink(handoffPath);
|
|
69
106
|
}
|
|
70
|
-
const documents = await this.loadAvailableDocuments(project);
|
|
71
|
-
const sourceCharacters = documents.reduce((sum, doc) => sum + doc.content.length, 0);
|
|
72
|
-
return this.buildHandoff(project, 'requirements', documents, sourceCharacters);
|
|
73
107
|
}
|
|
74
108
|
estimateContextSize(sourceCharacters, compactCharacters) {
|
|
75
|
-
const sourceTokens =
|
|
76
|
-
const compactTokens =
|
|
77
|
-
const reductionPercentage = sourceTokens === 0
|
|
78
|
-
? 0
|
|
79
|
-
: Math.max(0, Math.round((1 - compactTokens / sourceTokens) * 100));
|
|
109
|
+
const sourceTokens = Math.ceil(sourceCharacters / 4);
|
|
110
|
+
const compactTokens = Math.ceil(compactCharacters / 4);
|
|
80
111
|
return {
|
|
81
112
|
sourceCharacters,
|
|
82
113
|
sourceTokens,
|
|
83
114
|
compactCharacters,
|
|
84
115
|
compactTokens,
|
|
85
|
-
reductionPercentage
|
|
116
|
+
reductionPercentage: sourceTokens === 0 || compactTokens >= sourceTokens ? 0 : Math.round((1 - compactTokens / sourceTokens) * 100),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
async loadContextRequest(request) {
|
|
120
|
+
const { mode, budget } = this.resolveLoadOptions(request);
|
|
121
|
+
const resolver = new SpecPathResolver(this.fileSystem);
|
|
122
|
+
const resolved = await resolver.resolve(request.projectRoot, request.featureName);
|
|
123
|
+
const specPath = path.join(resolved.featureRoot, 'spec.json');
|
|
124
|
+
await resolver.assertContained(resolved.featureRoot, specPath);
|
|
125
|
+
const spec = await this.readSpec(specPath, request.featureName);
|
|
126
|
+
const selected = await this.selectState(resolved.featureRoot, spec, request, resolver);
|
|
127
|
+
const sourceCharacters = selected.documents.reduce((total, document) => total + document.content.length, 0);
|
|
128
|
+
const sourceFingerprint = this.hash(JSON.stringify({
|
|
129
|
+
schema: HANDOFF_SCHEMA,
|
|
130
|
+
phase: selected.phase,
|
|
131
|
+
status: selected.status,
|
|
132
|
+
approvals: spec.approvals,
|
|
133
|
+
reviewRequired: spec.reviewRequired,
|
|
134
|
+
reviewCompleted: spec.reviewCompleted,
|
|
135
|
+
implementation: spec.implementation,
|
|
136
|
+
sources: selected.documents.map(({ relativePath, content }) => [relativePath, content]),
|
|
137
|
+
}));
|
|
138
|
+
const fingerprint = this.hash(JSON.stringify({
|
|
139
|
+
sourceFingerprint,
|
|
140
|
+
mode,
|
|
141
|
+
budget,
|
|
142
|
+
includeUnapproved: request.includeUnapproved === true,
|
|
143
|
+
selection: SELECTION_VERSION,
|
|
144
|
+
}));
|
|
145
|
+
const canonical = mode === 'compact'
|
|
146
|
+
&& request.maxEstimatedTokens === undefined
|
|
147
|
+
&& request.includeUnapproved !== true;
|
|
148
|
+
const handoffPath = path.join(resolved.featureRoot, 'context', 'handoff.md');
|
|
149
|
+
const built = mode === 'full'
|
|
150
|
+
? this.buildFull(spec, selected, budget)
|
|
151
|
+
: this.buildBounded(spec, selected, budget, mode, sourceFingerprint, fingerprint, canonical);
|
|
152
|
+
const cached = await this.readCanonicalCache(canonical, handoffPath, resolved.featureRoot, resolver, built.content);
|
|
153
|
+
if (cached !== undefined) {
|
|
154
|
+
const result = this.resultFor(cached, mode, selected, sourceFingerprint, fingerprint, sourceCharacters, [], 'hit');
|
|
155
|
+
return this.applyEtag(result, request.ifNoneMatch);
|
|
156
|
+
}
|
|
157
|
+
await this.persistCanonical(canonical, handoffPath, built.content, resolved.featureRoot, resolver);
|
|
158
|
+
const result = this.resultFor(built.content, mode, selected, sourceFingerprint, fingerprint, sourceCharacters, built.omittedSources, 'regenerated');
|
|
159
|
+
this.logger.debug('Context payload prepared', {
|
|
160
|
+
featureName: request.featureName,
|
|
161
|
+
mode,
|
|
162
|
+
phase: selected.phase,
|
|
163
|
+
payloadEstimatedTokens: result.payloadEstimatedTokens,
|
|
164
|
+
});
|
|
165
|
+
return this.applyEtag(result, request.ifNoneMatch);
|
|
166
|
+
}
|
|
167
|
+
resolveLoadOptions(request) {
|
|
168
|
+
const mode = request.mode ?? 'compact';
|
|
169
|
+
const budget = request.maxEstimatedTokens ?? MODE_BUDGETS[mode];
|
|
170
|
+
if (!Number.isInteger(budget) || budget <= 0)
|
|
171
|
+
throw new ContextBudgetTooSmallError(budget, 1);
|
|
172
|
+
if (request.includeUnapproved && mode !== 'full') {
|
|
173
|
+
throw new Error('includeUnapproved is supported only in full mode');
|
|
174
|
+
}
|
|
175
|
+
return { mode, budget };
|
|
176
|
+
}
|
|
177
|
+
async readCanonicalCache(canonical, handoffPath, featureRoot, resolver, expectedContent) {
|
|
178
|
+
if (!canonical || !(await this.fileSystem.exists(handoffPath)))
|
|
179
|
+
return undefined;
|
|
180
|
+
await resolver.assertContained(featureRoot, handoffPath);
|
|
181
|
+
const cached = await this.fileSystem.readFile(handoffPath);
|
|
182
|
+
return cached === expectedContent ? cached : undefined;
|
|
183
|
+
}
|
|
184
|
+
async persistCanonical(canonical, handoffPath, content, featureRoot, resolver) {
|
|
185
|
+
if (!canonical)
|
|
186
|
+
return;
|
|
187
|
+
const contextRoot = path.dirname(handoffPath);
|
|
188
|
+
await resolver.assertContained(featureRoot, contextRoot);
|
|
189
|
+
await this.fileSystem.mkdir(contextRoot);
|
|
190
|
+
await resolver.assertContained(featureRoot, contextRoot);
|
|
191
|
+
await resolver.assertContained(featureRoot, handoffPath);
|
|
192
|
+
if (!this.fileSystem.writeFileAtomic) {
|
|
193
|
+
throw new Error('FileSystemPort.writeFileAtomic is required for context persistence');
|
|
194
|
+
}
|
|
195
|
+
await this.fileSystem.writeFileAtomic(handoffPath, content);
|
|
196
|
+
}
|
|
197
|
+
applyEtag(result, ifNoneMatch) {
|
|
198
|
+
if (ifNoneMatch !== result.fingerprint)
|
|
199
|
+
return result;
|
|
200
|
+
return {
|
|
201
|
+
...result,
|
|
202
|
+
content: undefined,
|
|
203
|
+
cacheStatus: 'not-modified',
|
|
204
|
+
payloadCharacters: 0,
|
|
205
|
+
payloadEstimatedTokens: 0,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
async readSpec(specPath, expectedFeature) {
|
|
209
|
+
if (!(await this.fileSystem.exists(specPath))) {
|
|
210
|
+
throw new ContextSourceError(specPath, 'Missing required workflow metadata');
|
|
211
|
+
}
|
|
212
|
+
const record = await this.readJsonRecord(specPath);
|
|
213
|
+
const approvalsRecord = this.requiredRecord(record.approvals, specPath, 'Missing approvals');
|
|
214
|
+
const options = this.optionalRecord(record.workflow_options ?? record.workflowOptions);
|
|
215
|
+
const checkpoints = this.optionalRecord(record.checkpoints);
|
|
216
|
+
const test = this.optionalRecord(checkpoints.test_cases ?? checkpoints.testCases);
|
|
217
|
+
const implementationRecord = this.optionalRecord(record.implementation);
|
|
218
|
+
const implementationTasks = this.optionalRecord(implementationRecord.tasks);
|
|
219
|
+
return {
|
|
220
|
+
featureName: this.specFeatureName(record, expectedFeature, specPath),
|
|
221
|
+
phase: typeof record.phase === 'string' ? record.phase : 'init',
|
|
222
|
+
approvals: this.parseApprovals(approvalsRecord),
|
|
223
|
+
reviewRequired: test.required === true
|
|
224
|
+
|| options.review_test_cases === true
|
|
225
|
+
|| options.reviewTestCases === true,
|
|
226
|
+
reviewCompleted: test.reviewed === true,
|
|
227
|
+
implementation: Object.keys(implementationTasks).length > 0
|
|
228
|
+
? {
|
|
229
|
+
revision: typeof implementationRecord.revision === 'number' ? implementationRecord.revision : 0,
|
|
230
|
+
tasks: Object.fromEntries(Object.entries(implementationTasks).map(([taskNumber, value]) => {
|
|
231
|
+
const task = this.optionalRecord(value);
|
|
232
|
+
const dependencies = Array.isArray(task.dependencies)
|
|
233
|
+
? task.dependencies.filter((dependency) => typeof dependency === 'string')
|
|
234
|
+
: [];
|
|
235
|
+
return [taskNumber, {
|
|
236
|
+
status: typeof task.status === 'string' ? task.status : 'pending',
|
|
237
|
+
blocker: typeof task.blocker === 'string' ? task.blocker : undefined,
|
|
238
|
+
dependencies,
|
|
239
|
+
}];
|
|
240
|
+
})),
|
|
241
|
+
}
|
|
242
|
+
: undefined,
|
|
86
243
|
};
|
|
87
244
|
}
|
|
88
|
-
async
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
...
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
? '
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
245
|
+
async readJsonRecord(specPath) {
|
|
246
|
+
let value;
|
|
247
|
+
try {
|
|
248
|
+
value = JSON.parse(await this.fileSystem.readFile(specPath));
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
throw new ContextSourceError(specPath, 'Malformed workflow metadata');
|
|
252
|
+
}
|
|
253
|
+
if (!value || typeof value !== 'object') {
|
|
254
|
+
throw new ContextSourceError(specPath, 'Invalid workflow metadata');
|
|
255
|
+
}
|
|
256
|
+
return value;
|
|
257
|
+
}
|
|
258
|
+
requiredRecord(value, specPath, message) {
|
|
259
|
+
if (!value || typeof value !== 'object')
|
|
260
|
+
throw new ContextSourceError(specPath, message);
|
|
261
|
+
return value;
|
|
262
|
+
}
|
|
263
|
+
optionalRecord(value) {
|
|
264
|
+
return value && typeof value === 'object' ? value : {};
|
|
265
|
+
}
|
|
266
|
+
parseApprovals(record) {
|
|
267
|
+
return Object.fromEntries(PHASES.map(phase => {
|
|
268
|
+
const state = this.optionalRecord(record[phase]);
|
|
269
|
+
return [phase, {
|
|
270
|
+
generated: state.generated === true,
|
|
271
|
+
approved: state.approved === true,
|
|
272
|
+
artifactSha256: typeof state.artifact_sha256 === 'string'
|
|
273
|
+
? state.artifact_sha256
|
|
274
|
+
: typeof state.artifactSha256 === 'string'
|
|
275
|
+
? state.artifactSha256
|
|
276
|
+
: undefined,
|
|
277
|
+
}];
|
|
278
|
+
}));
|
|
279
|
+
}
|
|
280
|
+
specFeatureName(record, expectedFeature, specPath) {
|
|
281
|
+
const persisted = typeof record.feature_name === 'string'
|
|
282
|
+
? record.feature_name
|
|
283
|
+
: typeof record.name === 'string'
|
|
284
|
+
? record.name
|
|
285
|
+
: undefined;
|
|
286
|
+
if (persisted !== undefined && persisted !== expectedFeature) {
|
|
287
|
+
throw new ContextSourceError(specPath, 'Feature metadata name does not match directory');
|
|
288
|
+
}
|
|
289
|
+
return expectedFeature;
|
|
290
|
+
}
|
|
291
|
+
async selectState(featureRoot, spec, request, resolver) {
|
|
292
|
+
const selection = this.resolveEffectivePhase(spec, request);
|
|
293
|
+
const documents = await this.readSelectedDocuments(featureRoot, spec, selection.phase, resolver);
|
|
294
|
+
return { ...selection, documents };
|
|
295
|
+
}
|
|
296
|
+
resolveEffectivePhase(spec, request) {
|
|
297
|
+
if (spec.implementation && !request.phase)
|
|
298
|
+
return { phase: 'implementation', status: 'approved' };
|
|
299
|
+
if (request.phase)
|
|
300
|
+
return this.resolveExplicitPhase(spec, request);
|
|
301
|
+
if (request.mode === 'full' && request.includeUnapproved) {
|
|
302
|
+
const latestGenerated = [...PHASES].reverse()
|
|
303
|
+
.find(candidate => spec.approvals[candidate].generated);
|
|
304
|
+
if (!latestGenerated)
|
|
305
|
+
return { phase: 'init', status: 'init' };
|
|
306
|
+
return {
|
|
307
|
+
phase: latestGenerated,
|
|
308
|
+
status: spec.approvals[latestGenerated].approved ? 'approved' : 'unapproved',
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
const latestApproved = [...PHASES].reverse()
|
|
312
|
+
.find(candidate => spec.approvals[candidate].approved);
|
|
313
|
+
return latestApproved
|
|
314
|
+
? { phase: latestApproved, status: 'approved' }
|
|
315
|
+
: { phase: 'init', status: 'init' };
|
|
316
|
+
}
|
|
317
|
+
resolveExplicitPhase(spec, request) {
|
|
318
|
+
const phase = request.phase;
|
|
319
|
+
if (!phase)
|
|
320
|
+
return { phase: 'init', status: 'init' };
|
|
321
|
+
if (phase === 'implementation') {
|
|
322
|
+
if (!spec.approvals.tasks.approved || !spec.implementation)
|
|
323
|
+
throw new PhaseNotApprovedError('tasks');
|
|
324
|
+
return { phase, status: 'approved' };
|
|
325
|
+
}
|
|
326
|
+
const state = spec.approvals[phase];
|
|
327
|
+
const draftAllowed = request.mode === 'full' && request.includeUnapproved && state.generated;
|
|
328
|
+
if (!state.approved && !draftAllowed)
|
|
329
|
+
throw new PhaseNotApprovedError(phase);
|
|
330
|
+
return { phase, status: state.approved ? 'approved' : 'unapproved' };
|
|
331
|
+
}
|
|
332
|
+
async readSelectedDocuments(featureRoot, spec, phase, resolver) {
|
|
333
|
+
if (phase === 'init')
|
|
334
|
+
return [];
|
|
158
335
|
const documents = [];
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
documents.push({ name, path, content });
|
|
336
|
+
const last = phase === 'implementation' ? PHASES.length - 1 : PHASES.indexOf(phase);
|
|
337
|
+
for (const selectedPhase of PHASES.slice(0, last + 1)) {
|
|
338
|
+
if (!spec.approvals[selectedPhase].generated) {
|
|
339
|
+
throw new ContextSourceError(`${selectedPhase}.md`, 'Selected phase metadata is inconsistent');
|
|
164
340
|
}
|
|
341
|
+
documents.push(await this.readPhaseDocument(featureRoot, selectedPhase, spec.approvals[selectedPhase], resolver));
|
|
165
342
|
}
|
|
166
343
|
return documents;
|
|
167
344
|
}
|
|
168
|
-
async
|
|
345
|
+
async readPhaseDocument(featureRoot, selectedPhase, approval, resolver) {
|
|
346
|
+
const name = `${selectedPhase}.md`;
|
|
347
|
+
const absolutePath = path.join(featureRoot, name);
|
|
348
|
+
await resolver.assertContained(featureRoot, absolutePath);
|
|
349
|
+
if (!(await this.fileSystem.exists(absolutePath))) {
|
|
350
|
+
throw new ContextSourceError(absolutePath, 'Missing required selected-phase document');
|
|
351
|
+
}
|
|
352
|
+
let content;
|
|
169
353
|
try {
|
|
170
|
-
|
|
171
|
-
return null;
|
|
172
|
-
}
|
|
173
|
-
return await this.fileSystem.readFile(path);
|
|
354
|
+
content = await this.fileSystem.readFile(absolutePath);
|
|
174
355
|
}
|
|
175
356
|
catch {
|
|
176
|
-
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
extractRequirementLikeLines(lines) {
|
|
197
|
-
const keywords = /\b(SHALL|MUST|WHEN|IF|THEN|WHERE|constraint|risk|security|performance|error|edge case)\b/i;
|
|
198
|
-
return this.unique(lines
|
|
199
|
-
.filter((line) => keywords.test(line))
|
|
200
|
-
.map((line) => line.replace(/^[-*]\s+/, '').replace(/^\d+\.\s+/, '').trim())
|
|
201
|
-
.filter((line) => line.length <= 240));
|
|
202
|
-
}
|
|
203
|
-
unique(lines) {
|
|
204
|
-
const seen = new Set();
|
|
205
|
-
const result = [];
|
|
206
|
-
for (const line of lines) {
|
|
207
|
-
const key = line.toLowerCase();
|
|
208
|
-
if (!seen.has(key)) {
|
|
209
|
-
seen.add(key);
|
|
210
|
-
result.push(line);
|
|
211
|
-
}
|
|
357
|
+
throw new ContextSourceError(absolutePath, 'Unable to read selected-phase document');
|
|
358
|
+
}
|
|
359
|
+
if (approval.approved && approval.artifactSha256 && this.hash(content) !== approval.artifactSha256) {
|
|
360
|
+
throw new ContextArtifactDriftError(selectedPhase);
|
|
361
|
+
}
|
|
362
|
+
return {
|
|
363
|
+
name,
|
|
364
|
+
relativePath: name,
|
|
365
|
+
absolutePath,
|
|
366
|
+
content,
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
buildBounded(spec, selected, budget, mode, sourceFingerprint, fingerprint, canonical) {
|
|
370
|
+
const envelope = this.buildEnvelope(spec, selected, sourceFingerprint, fingerprint, canonical);
|
|
371
|
+
const maximumCharacters = budget * 4;
|
|
372
|
+
if (envelope.length > maximumCharacters) {
|
|
373
|
+
throw new ContextBudgetTooSmallError(budget, Math.ceil(envelope.length / 4));
|
|
374
|
+
}
|
|
375
|
+
if (!selected.documents.length) {
|
|
376
|
+
return { content: this.embedEstimate(envelope), omittedSources: [] };
|
|
212
377
|
}
|
|
213
|
-
|
|
378
|
+
const direct = this.buildDirectPayload(envelope, selected, maximumCharacters);
|
|
379
|
+
if (direct)
|
|
380
|
+
return { content: direct, omittedSources: [] };
|
|
381
|
+
return this.selectCandidates(envelope, this.collectCandidates(selected.documents, mode), selected.documents, maximumCharacters);
|
|
382
|
+
}
|
|
383
|
+
buildEnvelope(spec, selected, sourceFingerprint, fingerprint, canonical) {
|
|
384
|
+
const metadata = canonical
|
|
385
|
+
? `<!-- sdd-context schema=${HANDOFF_SCHEMA} phase=${selected.phase} source=${sourceFingerprint} payload=${fingerprint} -->\n`
|
|
386
|
+
: '';
|
|
387
|
+
const sourceReferences = selected.documents.length
|
|
388
|
+
? selected.documents.map(document => `- ${document.relativePath}`).join('\n')
|
|
389
|
+
: '- spec.json';
|
|
390
|
+
const review = spec.reviewRequired
|
|
391
|
+
? spec.reviewCompleted ? 'reviewed' : 'pending'
|
|
392
|
+
: 'not required';
|
|
393
|
+
const implementationProgress = spec.implementation
|
|
394
|
+
? `\n\n## Implementation Progress\n${this.implementationProgress(spec)}`
|
|
395
|
+
: '';
|
|
396
|
+
return `${metadata}# SDD Context: ${spec.featureName}\n\n## Workflow State\n- Effective phase: ${selected.phase}\n- Phase status: ${selected.status}\n- Requirements: ${this.approvalLabel(spec.approvals.requirements)}\n- Design: ${this.approvalLabel(spec.approvals.design)}\n- Tasks: ${this.approvalLabel(spec.approvals.tasks)}\n- Test-case review: ${review}${implementationProgress}\n\n## Next Action\n${this.nextAction(spec, selected)}\n\n## Source References\n${sourceReferences}\n\n## Payload Estimate\n- Payload estimated tokens: 00000`;
|
|
214
397
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
398
|
+
nextAction(spec, selected) {
|
|
399
|
+
const phase = selected.phase;
|
|
400
|
+
if (selected.status === 'unapproved') {
|
|
401
|
+
return `Continue the ${phase} Skill to revise, validate, and request approval for this draft.`;
|
|
218
402
|
}
|
|
219
|
-
if (
|
|
220
|
-
return
|
|
403
|
+
if (phase === 'init')
|
|
404
|
+
return 'Generate requirements through the requirements Skill.';
|
|
405
|
+
if (phase === 'requirements')
|
|
406
|
+
return 'Continue with the design Skill.';
|
|
407
|
+
if (phase === 'design')
|
|
408
|
+
return 'Continue with the tasks Skill.';
|
|
409
|
+
if (phase === 'implementation' && spec.implementation) {
|
|
410
|
+
const tasks = Object.entries(spec.implementation.tasks);
|
|
411
|
+
const active = tasks.filter(([, task]) => ['in-progress', 'red-observed', 'green-observed', 'blocked'].includes(task.status));
|
|
412
|
+
if (active.length === 1)
|
|
413
|
+
return `Continue task ${active[0][0]} from ${active[0][1].status}.`;
|
|
414
|
+
if (active.length > 1) {
|
|
415
|
+
return `Select a resumable task: ${this.taskCandidateSummary(active, true)}.`;
|
|
416
|
+
}
|
|
417
|
+
const ready = tasks.filter(([, task]) => task.status === 'pending'
|
|
418
|
+
&& task.dependencies.every((dependency) => spec.implementation.tasks[dependency]?.status === 'completed'));
|
|
419
|
+
if (ready.length === 1)
|
|
420
|
+
return `Start task ${ready[0][0]}.`;
|
|
421
|
+
if (ready.length > 1)
|
|
422
|
+
return `Select a ready task: ${this.taskCandidateSummary(ready, false)}.`;
|
|
423
|
+
return tasks.every(([, task]) => task.status === 'completed')
|
|
424
|
+
? 'Implementation is complete.'
|
|
425
|
+
: 'No dependency-ready task is available; inspect persisted task blockers.';
|
|
221
426
|
}
|
|
222
|
-
if (
|
|
223
|
-
return
|
|
427
|
+
if (spec.reviewRequired && !spec.reviewCompleted)
|
|
428
|
+
return 'Review test cases before approving tasks.';
|
|
429
|
+
return 'Start governed implementation.';
|
|
430
|
+
}
|
|
431
|
+
taskCandidateSummary(candidates, includeStatus) {
|
|
432
|
+
const visible = candidates.slice(0, 20).map(([id, task]) => includeStatus ? `${id} (${task.status})` : id);
|
|
433
|
+
const omitted = candidates.length - visible.length;
|
|
434
|
+
return `${visible.join(', ')}${omitted > 0 ? `, and ${omitted} more` : ''}`;
|
|
435
|
+
}
|
|
436
|
+
implementationProgress(spec) {
|
|
437
|
+
if (!spec.implementation)
|
|
438
|
+
return '';
|
|
439
|
+
const tasks = Object.entries(spec.implementation.tasks);
|
|
440
|
+
const completed = tasks.filter(([, task]) => task.status === 'completed').length;
|
|
441
|
+
const active = tasks.filter(([, task]) => ['in-progress', 'red-observed', 'green-observed'].includes(task.status));
|
|
442
|
+
const blocked = tasks.filter(([, task]) => task.status === 'blocked');
|
|
443
|
+
const details = [...active, ...blocked].slice(0, 20)
|
|
444
|
+
.map(([taskNumber, task]) => `- ${taskNumber}: ${task.status}${this.blockerSummary(task.blocker)}`)
|
|
445
|
+
.join('\n');
|
|
446
|
+
return `- Revision: ${spec.implementation.revision}\n- Completed: ${completed}/${tasks.length}\n- Active: ${active.length}\n- Blocked: ${blocked.length}${details ? `\n${details}` : ''}`;
|
|
447
|
+
}
|
|
448
|
+
blockerSummary(blocker) {
|
|
449
|
+
if (!blocker)
|
|
450
|
+
return '';
|
|
451
|
+
const normalized = blocker.replace(/\s+/g, ' ').trim();
|
|
452
|
+
const summary = normalized.length > 160 ? `${normalized.slice(0, 159)}…` : normalized;
|
|
453
|
+
return ` — ${summary}`;
|
|
454
|
+
}
|
|
455
|
+
buildDirectPayload(envelope, selected, maximumCharacters) {
|
|
456
|
+
if (selected.documents.length !== 1 || selected.documents[0].content.length > 600) {
|
|
457
|
+
return undefined;
|
|
224
458
|
}
|
|
225
|
-
|
|
459
|
+
const direct = `${envelope}\n\n## Approved Source\n${selected.documents[0].content.trim()}`;
|
|
460
|
+
return direct.length <= maximumCharacters ? this.embedEstimate(direct) : undefined;
|
|
226
461
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
462
|
+
collectCandidates(documents, mode) {
|
|
463
|
+
const seen = new Set();
|
|
464
|
+
const candidates = [];
|
|
465
|
+
const perLineCap = mode === 'compact' ? 320 : 640;
|
|
466
|
+
for (const document of documents) {
|
|
467
|
+
this.collectDocumentCandidates(document, perLineCap, seen, candidates);
|
|
230
468
|
}
|
|
231
|
-
|
|
232
|
-
|
|
469
|
+
return candidates;
|
|
470
|
+
}
|
|
471
|
+
collectDocumentCandidates(document, perLineCap, seen, candidates) {
|
|
472
|
+
for (const rawLine of document.content.split(/\r?\n/)) {
|
|
473
|
+
const normalized = rawLine.trim().replace(/^[-*]\s+/, '').replace(/^\d+\.\s+/, '');
|
|
474
|
+
if (!normalized || normalized.startsWith('```'))
|
|
475
|
+
continue;
|
|
476
|
+
const capped = normalized.slice(0, perLineCap);
|
|
477
|
+
const key = capped.toLocaleLowerCase();
|
|
478
|
+
if (seen.has(key))
|
|
479
|
+
continue;
|
|
480
|
+
seen.add(key);
|
|
481
|
+
candidates.push({ source: document.relativePath, text: capped });
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
selectCandidates(envelope, candidates, documents, maximumCharacters) {
|
|
485
|
+
let content = envelope;
|
|
486
|
+
const usedSources = new Set();
|
|
487
|
+
for (const candidate of candidates) {
|
|
488
|
+
const line = `\n- ${candidate.text}`;
|
|
489
|
+
if (content.length + '\n\n## Selected Context'.length + line.length > maximumCharacters) {
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
if (!content.includes('\n\n## Selected Context'))
|
|
493
|
+
content += '\n\n## Selected Context';
|
|
494
|
+
content += line;
|
|
495
|
+
usedSources.add(candidate.source);
|
|
233
496
|
}
|
|
234
|
-
return
|
|
497
|
+
return {
|
|
498
|
+
content: this.embedEstimate(content),
|
|
499
|
+
omittedSources: documents
|
|
500
|
+
.filter(document => !usedSources.has(document.relativePath))
|
|
501
|
+
.map(document => document.relativePath),
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
buildFull(spec, selected, budget) {
|
|
505
|
+
let content = [
|
|
506
|
+
`# Full SDD Context: ${spec.featureName}`,
|
|
507
|
+
`Effective phase: ${selected.phase} (${selected.status})`,
|
|
508
|
+
`Next action: ${this.nextAction(spec, selected)}`,
|
|
509
|
+
...selected.documents.map((document) => `## ${document.name}\n\n${document.content}`),
|
|
510
|
+
'## Payload Estimate\n- Payload estimated tokens: 00000',
|
|
511
|
+
].join('\n\n');
|
|
512
|
+
content = this.embedEstimate(content);
|
|
513
|
+
const required = Math.ceil(content.length / 4);
|
|
514
|
+
if (required > budget)
|
|
515
|
+
throw new ContextBudgetExceededError(budget, required);
|
|
516
|
+
return { content, omittedSources: [] };
|
|
517
|
+
}
|
|
518
|
+
embedEstimate(content) {
|
|
519
|
+
return content.replace('Payload estimated tokens: 00000', `Payload estimated tokens: ${String(Math.ceil(content.length / 4)).padStart(5, '0')}`);
|
|
235
520
|
}
|
|
236
|
-
|
|
237
|
-
|
|
521
|
+
resultFor(content, mode, selected, sourceFingerprint, fingerprint, sourceCharacters, omittedSources, cacheStatus) {
|
|
522
|
+
const payloadEstimatedTokens = Math.ceil(content.length / 4);
|
|
523
|
+
const sourceEstimatedTokens = Math.ceil(sourceCharacters / 4);
|
|
524
|
+
return { content, mode, effectivePhase: selected.phase, phaseStatus: selected.status, sourceFingerprint, fingerprint, cacheStatus, sourceCharacters, sourceEstimatedTokens, payloadCharacters: content.length, payloadEstimatedTokens, reductionPercentage: sourceEstimatedTokens === 0 || payloadEstimatedTokens >= sourceEstimatedTokens ? 0 : Math.round((1 - payloadEstimatedTokens / sourceEstimatedTokens) * 100), omittedSources };
|
|
238
525
|
}
|
|
239
|
-
|
|
240
|
-
return
|
|
526
|
+
approvalLabel(state) {
|
|
527
|
+
return state.approved ? 'approved' : state.generated ? 'generated, unapproved' : 'not generated';
|
|
241
528
|
}
|
|
242
|
-
|
|
243
|
-
return
|
|
529
|
+
hash(content) {
|
|
530
|
+
return createHash('sha256').update(content).digest('hex');
|
|
244
531
|
}
|
|
245
532
|
};
|
|
246
533
|
ContextCompactionService = __decorate([
|