vgxness 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/LICENSE +9 -0
  2. package/README.md +110 -0
  3. package/dist/agents/agent-activation-service.js +144 -0
  4. package/dist/agents/agent-registry-service.js +46 -0
  5. package/dist/agents/agent-resolver.js +249 -0
  6. package/dist/agents/agent-seed-service.js +146 -0
  7. package/dist/agents/manager-profile-overlay-service.js +34 -0
  8. package/dist/agents/profile-model-routing.js +26 -0
  9. package/dist/agents/renderers/claude-renderer.js +98 -0
  10. package/dist/agents/renderers/index.js +16 -0
  11. package/dist/agents/renderers/json-renderer.js +87 -0
  12. package/dist/agents/renderers/opencode-renderer.js +100 -0
  13. package/dist/agents/renderers/provider-adapter.js +6 -0
  14. package/dist/agents/repositories/agents.js +185 -0
  15. package/dist/agents/repositories/manager-profile-overlays.js +81 -0
  16. package/dist/agents/schema.js +1 -0
  17. package/dist/cli/dashboard-operational-read-models.js +153 -0
  18. package/dist/cli/dashboard-renderer.js +109 -0
  19. package/dist/cli/dashboard-screen-renderers.js +332 -0
  20. package/dist/cli/dashboard-tui-read-model.js +71 -0
  21. package/dist/cli/dashboard-tui-state.js +218 -0
  22. package/dist/cli/dispatcher.js +2880 -0
  23. package/dist/cli/index.js +27 -0
  24. package/dist/cli/interactive-dashboard.js +29 -0
  25. package/dist/cli/mcp-start-path.js +21 -0
  26. package/dist/cli/setup-status-renderer.js +29 -0
  27. package/dist/cli/setup-wizard-read-model.js +56 -0
  28. package/dist/cli/setup-wizard-renderer.js +148 -0
  29. package/dist/cli/setup-wizard-state.js +82 -0
  30. package/dist/cli/tui-render-helpers.js +192 -0
  31. package/dist/export/redaction.js +71 -0
  32. package/dist/harness/tools/agents.js +245 -0
  33. package/dist/harness/tools/memory.js +29 -0
  34. package/dist/mcp/client-install-opencode-contract.js +227 -0
  35. package/dist/mcp/client-install-opencode.js +194 -0
  36. package/dist/mcp/client-setup-preview.js +38 -0
  37. package/dist/mcp/control-plane.js +175 -0
  38. package/dist/mcp/doctor.js +193 -0
  39. package/dist/mcp/index.js +10 -0
  40. package/dist/mcp/opencode-default-agent-config.js +156 -0
  41. package/dist/mcp/opencode-visibility.js +102 -0
  42. package/dist/mcp/schema.js +234 -0
  43. package/dist/mcp/stdio-server.js +56 -0
  44. package/dist/mcp/validation.js +761 -0
  45. package/dist/memory/import/dry-run-planner.js +58 -0
  46. package/dist/memory/import/index.js +3 -0
  47. package/dist/memory/import/observation-writer.js +220 -0
  48. package/dist/memory/import/package.js +178 -0
  49. package/dist/memory/memory-service.js +126 -0
  50. package/dist/memory/repositories/artifacts.js +41 -0
  51. package/dist/memory/repositories/observations.js +133 -0
  52. package/dist/memory/repositories/sessions.js +105 -0
  53. package/dist/memory/repositories/traces.js +58 -0
  54. package/dist/memory/schema.js +1 -0
  55. package/dist/memory/search.js +11 -0
  56. package/dist/memory/sqlite/database.js +97 -0
  57. package/dist/memory/sqlite/migrations/001_initial.sql +128 -0
  58. package/dist/memory/sqlite/migrations/002_observation_revisions.sql +14 -0
  59. package/dist/memory/sqlite/migrations/003_agent_registry.sql +26 -0
  60. package/dist/memory/sqlite/migrations/004_run_runtime.sql +62 -0
  61. package/dist/memory/sqlite/migrations/005_run_approvals.sql +20 -0
  62. package/dist/memory/sqlite/migrations/006_run_operation_attempts.sql +32 -0
  63. package/dist/memory/sqlite/migrations/007_abandoned_operation_attempts.sql +46 -0
  64. package/dist/memory/sqlite/migrations/008_run_execution_plan_events.sql +105 -0
  65. package/dist/memory/sqlite/migrations/009_multiple_operation_attempts.sql +73 -0
  66. package/dist/memory/sqlite/migrations/010_skill_registry.sql +66 -0
  67. package/dist/memory/sqlite/migrations/011_skill_usage_resolution_outcomes.sql +21 -0
  68. package/dist/memory/sqlite/migrations/012_skill_improvement_proposals.sql +37 -0
  69. package/dist/memory/sqlite/migrations/013_skill_evaluation_scenarios.sql +43 -0
  70. package/dist/memory/sqlite/migrations/014_manager_profile_overlays.sql +14 -0
  71. package/dist/memory/storage-paths.js +72 -0
  72. package/dist/orchestrator/natural-language-planner.js +191 -0
  73. package/dist/orchestrator/schema.js +1 -0
  74. package/dist/permissions/index.js +2 -0
  75. package/dist/permissions/policy-evaluator.js +109 -0
  76. package/dist/permissions/schema.js +1 -0
  77. package/dist/providers/opencode/injection-preview.js +134 -0
  78. package/dist/providers/opencode/manager-payload.js +129 -0
  79. package/dist/runs/execution-planning.js +117 -0
  80. package/dist/runs/operation-execution.js +1 -0
  81. package/dist/runs/operation-retry.js +124 -0
  82. package/dist/runs/repositories/runs.js +611 -0
  83. package/dist/runs/run-insights.js +145 -0
  84. package/dist/runs/run-service.js +713 -0
  85. package/dist/runs/run-snapshot-export-service.js +31 -0
  86. package/dist/runs/sandbox-process-execution.js +218 -0
  87. package/dist/runs/sandbox-worktree-planning.js +59 -0
  88. package/dist/runs/schema.js +1 -0
  89. package/dist/sdd/artifact-portability-service.js +118 -0
  90. package/dist/sdd/schema.js +17 -0
  91. package/dist/sdd/sdd-workflow-service.js +217 -0
  92. package/dist/setup/backup-rollback-service.js +76 -0
  93. package/dist/setup/index.js +3 -0
  94. package/dist/setup/providers/antigravity-setup-adapter.js +18 -0
  95. package/dist/setup/providers/claude-setup-adapter.js +30 -0
  96. package/dist/setup/providers/custom-setup-adapter.js +18 -0
  97. package/dist/setup/providers/index.js +6 -0
  98. package/dist/setup/providers/opencode-setup-adapter.js +104 -0
  99. package/dist/setup/providers/provider-setup-adapter.js +15 -0
  100. package/dist/setup/providers/provider-setup-registry.js +11 -0
  101. package/dist/setup/schema.js +1 -0
  102. package/dist/setup/setup-defaults.js +11 -0
  103. package/dist/setup/setup-lifecycle-service.js +175 -0
  104. package/dist/setup/setup-plan.js +105 -0
  105. package/dist/skills/repositories/skill-evaluation-scenarios.js +289 -0
  106. package/dist/skills/repositories/skill-improvement-proposals.js +288 -0
  107. package/dist/skills/repositories/skills.js +430 -0
  108. package/dist/skills/schema.js +1 -0
  109. package/dist/skills/skill-payload.js +94 -0
  110. package/dist/skills/skill-registry-service.js +92 -0
  111. package/dist/skills/skill-resolver.js +191 -0
  112. package/dist/workflows/command-allowlist-adapter.js +70 -0
  113. package/dist/workflows/schema.js +4 -0
  114. package/dist/workflows/workflow-executor.js +345 -0
  115. package/dist/workflows/workflow-registry.js +66 -0
  116. package/docs/architecture.md +698 -0
  117. package/docs/cli.md +741 -0
  118. package/docs/funcionamiento-del-sistema.md +868 -0
  119. package/docs/harness-gap-analysis.md +229 -0
  120. package/docs/prd.md +372 -0
  121. package/package.json +57 -0
@@ -0,0 +1,58 @@
1
+ import { validateMemoryImportPackage } from './package.js';
2
+ export function planMemoryImportDryRun(input) {
3
+ const validation = validateMemoryImportPackage(input.packageJson);
4
+ if (!validation.ok)
5
+ return validationFailure(`Invalid memory import package: ${formatValidationErrors(validation.errors)}`);
6
+ const packageValue = validation.package;
7
+ return {
8
+ ok: true,
9
+ value: {
10
+ mode: 'dry-run',
11
+ migrationBatchId: packageValue.migrationBatchId,
12
+ source: packageValue.source,
13
+ target: {
14
+ databasePath: input.targetDatabase.path,
15
+ databasePathSource: input.targetDatabase.source,
16
+ },
17
+ counts: {
18
+ observations: packageValue.observations.length,
19
+ sddArtifacts: packageValue.sddArtifacts?.length ?? 0,
20
+ sessionSummaries: packageValue.sessionSummaries?.length ?? 0,
21
+ },
22
+ plannedCreates: {
23
+ observations: packageValue.observations.map((observation) => ({
24
+ project: observation.project,
25
+ scope: observation.scope,
26
+ type: observation.type,
27
+ title: observation.title,
28
+ ...(observation.topicKey !== undefined ? { topicKey: observation.topicKey } : {}),
29
+ })),
30
+ sddArtifacts: (packageValue.sddArtifacts ?? []).map((artifact) => ({
31
+ project: artifact.project,
32
+ change: artifact.change,
33
+ phase: artifact.phase,
34
+ ...(artifact.topicKey !== undefined ? { topicKey: artifact.topicKey } : {}),
35
+ })),
36
+ sessionSummaries: (packageValue.sessionSummaries ?? []).map((summary) => ({
37
+ ...(summary.project !== undefined ? { project: summary.project } : {}),
38
+ ...(summary.sourceSessionId !== undefined ? { sourceSessionId: summary.sourceSessionId } : {}),
39
+ ...(summary.sessionId !== undefined ? { sessionId: summary.sessionId } : {}),
40
+ ...(summary.topicKey !== undefined ? { topicKey: summary.topicKey } : {}),
41
+ })),
42
+ },
43
+ safety: {
44
+ mutatesDatabase: false,
45
+ writesObservations: false,
46
+ writesSddArtifacts: false,
47
+ writesSessionSummaries: false,
48
+ writesTraces: false,
49
+ },
50
+ },
51
+ };
52
+ }
53
+ function formatValidationErrors(errors) {
54
+ return errors.map((error) => `${error.path}: ${error.message}`).join('; ');
55
+ }
56
+ function validationFailure(message) {
57
+ return { ok: false, error: { code: 'validation_failed', message } };
58
+ }
@@ -0,0 +1,3 @@
1
+ export * from './package.js';
2
+ export * from './dry-run-planner.js';
3
+ export * from './observation-writer.js';
@@ -0,0 +1,220 @@
1
+ import { SddWorkflowService } from '../../sdd/sdd-workflow-service.js';
2
+ import { sddTopicKey } from '../../sdd/schema.js';
3
+ import { validateMemoryImportPackage } from './package.js';
4
+ const context = { actor: 'memory-import' };
5
+ export function writeMemoryImportObservations(input) {
6
+ const validation = validateMemoryImportPackage(input.packageJson);
7
+ if (!validation.ok)
8
+ return validationFailure(`Invalid memory import package: ${formatValidationErrors(validation.errors)}`);
9
+ const packageValue = validation.package;
10
+ const summary = {
11
+ mode: 'write',
12
+ duplicatePolicy: input.duplicatePolicy,
13
+ migrationBatchId: packageValue.migrationBatchId,
14
+ source: packageValue.source,
15
+ target: {
16
+ databasePath: input.targetDatabase.path,
17
+ databasePathSource: input.targetDatabase.source,
18
+ },
19
+ counts: {
20
+ observations: emptyCounts(),
21
+ sddArtifacts: emptyCounts(),
22
+ sessionSummaries: emptyCounts(),
23
+ },
24
+ observations: { created: [], skipped: [], overwritten: [], failed: [] },
25
+ sddArtifacts: { created: [], skipped: [], overwritten: [], failed: [] },
26
+ sessionSummaries: { created: [], skipped: [], overwritten: [], failed: [] },
27
+ };
28
+ const written = input.service.transaction(() => {
29
+ for (const [index, observation] of packageValue.observations.entries()) {
30
+ const prepared = prepareObservation(observation, packageValue.migrationBatchId, index);
31
+ const existing = input.service.observations.getByTopic(prepared);
32
+ if (!existing.ok && existing.error.code !== 'not_found') {
33
+ summary.counts.observations.failed += 1;
34
+ summary.observations.failed.push({ ...identity(prepared), message: existing.error.message });
35
+ return { ok: false, error: existing.error };
36
+ }
37
+ if (existing.ok && input.duplicatePolicy === 'skip-existing') {
38
+ summary.counts.observations.skipped += 1;
39
+ summary.observations.skipped.push({ id: existing.value.id, ...identity(prepared) });
40
+ continue;
41
+ }
42
+ const saved = input.service.saveObservation(prepared, context);
43
+ if (!saved.ok) {
44
+ summary.counts.observations.failed += 1;
45
+ summary.observations.failed.push({ ...identity(prepared), message: saved.error.message });
46
+ return { ok: false, error: saved.error };
47
+ }
48
+ if (existing.ok) {
49
+ summary.counts.observations.overwritten += 1;
50
+ summary.observations.overwritten.push({ id: saved.value.id, ...identity(prepared) });
51
+ }
52
+ else {
53
+ summary.counts.observations.created += 1;
54
+ summary.observations.created.push({ id: saved.value.id, ...identity(prepared) });
55
+ }
56
+ }
57
+ const sdd = new SddWorkflowService(input.service, context);
58
+ for (const artifact of packageValue.sddArtifacts ?? []) {
59
+ const prepared = prepareSddArtifact(artifact, packageValue.migrationBatchId);
60
+ const existing = input.service.getArtifactNoTrace(prepared.project, prepared.topicKey);
61
+ if (!existing.ok && existing.error.code !== 'not_found') {
62
+ summary.counts.sddArtifacts.failed += 1;
63
+ summary.sddArtifacts.failed.push({ ...artifactIdentity(prepared), message: existing.error.message });
64
+ return { ok: false, error: existing.error };
65
+ }
66
+ if (existing.ok && input.duplicatePolicy === 'skip-existing') {
67
+ summary.counts.sddArtifacts.skipped += 1;
68
+ summary.sddArtifacts.skipped.push({ id: existing.value.id, ...artifactIdentity(prepared) });
69
+ continue;
70
+ }
71
+ const saved = sdd.saveArtifact(prepared);
72
+ if (!saved.ok) {
73
+ summary.counts.sddArtifacts.failed += 1;
74
+ summary.sddArtifacts.failed.push({ ...artifactIdentity(prepared), message: saved.error.message });
75
+ return { ok: false, error: saved.error };
76
+ }
77
+ if (existing.ok) {
78
+ summary.counts.sddArtifacts.overwritten += 1;
79
+ summary.sddArtifacts.overwritten.push({ id: saved.value.id, ...artifactIdentity(prepared) });
80
+ }
81
+ else {
82
+ summary.counts.sddArtifacts.created += 1;
83
+ summary.sddArtifacts.created.push({ id: saved.value.id, ...artifactIdentity(prepared) });
84
+ }
85
+ }
86
+ for (const [index, sessionSummary] of (packageValue.sessionSummaries ?? []).entries()) {
87
+ const prepared = prepareSessionSummary(sessionSummary, packageValue.migrationBatchId, index);
88
+ const existing = getExistingSessionSummary(input.service, prepared);
89
+ if (!existing.ok && existing.error.code !== 'not_found') {
90
+ summary.counts.sessionSummaries.failed += 1;
91
+ summary.sessionSummaries.failed.push({ ...identity(prepared), message: existing.error.message });
92
+ return { ok: false, error: existing.error };
93
+ }
94
+ if (existing.ok && input.duplicatePolicy === 'skip-existing') {
95
+ summary.counts.sessionSummaries.skipped += 1;
96
+ summary.sessionSummaries.skipped.push({ id: existing.value.id, ...identity(prepared) });
97
+ continue;
98
+ }
99
+ const saved = existing.ok && input.duplicatePolicy === 'overwrite'
100
+ ? input.service.updateObservation({ id: existing.value.id, type: prepared.type, title: prepared.title, content: prepared.content, topicKey: prepared.topicKey }, context)
101
+ : input.service.saveObservation(prepared, context);
102
+ if (!saved.ok) {
103
+ summary.counts.sessionSummaries.failed += 1;
104
+ summary.sessionSummaries.failed.push({ ...identity(prepared), message: saved.error.message });
105
+ return { ok: false, error: saved.error };
106
+ }
107
+ if (existing.ok) {
108
+ summary.counts.sessionSummaries.overwritten += 1;
109
+ summary.sessionSummaries.overwritten.push({ id: saved.value.id, ...identity(prepared) });
110
+ }
111
+ else {
112
+ summary.counts.sessionSummaries.created += 1;
113
+ summary.sessionSummaries.created.push({ id: saved.value.id, ...identity(prepared) });
114
+ }
115
+ }
116
+ return { ok: true, value: summary };
117
+ });
118
+ return written.ok ? { ok: true, value: written.value } : written;
119
+ }
120
+ function emptyCounts() {
121
+ return { created: 0, skipped: 0, overwritten: 0, rejected: 0, failed: 0 };
122
+ }
123
+ function prepareObservation(observation, migrationBatchId, index) {
124
+ const topicKey = observation.topicKey ?? generatedTopicKey(observation, migrationBatchId, index);
125
+ return {
126
+ project: observation.project,
127
+ scope: observation.scope,
128
+ type: observation.type,
129
+ title: observation.title,
130
+ content: withProvenanceBlock(observation.content, observation.source, migrationBatchId),
131
+ topicKey,
132
+ };
133
+ }
134
+ function prepareSddArtifact(artifact, migrationBatchId) {
135
+ return {
136
+ project: artifact.project,
137
+ change: artifact.change,
138
+ phase: artifact.phase,
139
+ topicKey: sddTopicKey(artifact.change, artifact.phase),
140
+ content: withProvenanceBlock(artifact.content, artifact.source, migrationBatchId, 'sddArtifact'),
141
+ };
142
+ }
143
+ function prepareSessionSummary(summary, migrationBatchId, index) {
144
+ const project = summary.project ?? 'vgxness';
145
+ const topicKey = summary.topicKey ?? sessionSummaryTopicKey(summary, migrationBatchId, index);
146
+ const sessionIdentity = summary.sourceSessionId ?? summary.sessionId ?? summary.source?.sessionId;
147
+ return {
148
+ project,
149
+ scope: 'project',
150
+ type: 'manual',
151
+ title: sessionIdentity === undefined ? `Imported session summary ${index + 1}` : `Imported session summary ${sessionIdentity}`,
152
+ content: withProvenanceBlock(summary.content, sessionSummaryProvenance(summary), migrationBatchId, 'sessionSummary'),
153
+ topicKey,
154
+ };
155
+ }
156
+ function getExistingSessionSummary(service, prepared) {
157
+ const found = service.observations.search({ project: prepared.project, scope: prepared.scope, topicKey: prepared.topicKey, limit: 1 });
158
+ if (!found.ok)
159
+ return found;
160
+ const [existing] = found.value;
161
+ if (existing === undefined)
162
+ return { ok: false, error: { code: 'not_found', message: `Session summary topic not found: ${prepared.topicKey}` } };
163
+ return { ok: true, value: existing };
164
+ }
165
+ function sessionSummaryProvenance(summary) {
166
+ return stripUndefined({ ...summary.source, sourceSessionId: summary.sourceSessionId, sessionId: summary.sessionId });
167
+ }
168
+ function generatedTopicKey(observation, migrationBatchId, index) {
169
+ const sourceId = observation.source?.id ?? observation.source?.sourceId;
170
+ if (sourceId !== undefined && sourceId.trim().length > 0)
171
+ return `migrated/engram/${slug(sourceId)}`;
172
+ return `migrated/engram/${slug(migrationBatchId)}/observation-${index + 1}-${slug(observation.title)}`;
173
+ }
174
+ function sessionSummaryTopicKey(summary, migrationBatchId, index) {
175
+ const sourceSessionId = summary.sourceSessionId ?? summary.sessionId ?? summary.source?.sessionId;
176
+ if (sourceSessionId !== undefined && sourceSessionId.trim().length > 0)
177
+ return `migrated/engram/session-summary/${slug(sourceSessionId)}`;
178
+ const sourceId = summary.source?.id ?? summary.source?.sourceId;
179
+ if (sourceId !== undefined && sourceId.trim().length > 0)
180
+ return `migrated/engram/session-summary/${slug(sourceId)}`;
181
+ return `migrated/engram/${slug(migrationBatchId)}/session-summary-${index + 1}`;
182
+ }
183
+ function withProvenanceBlock(content, sourceProvenance, migrationBatchId, provenanceKey = 'observation') {
184
+ const provenance = stableJson({
185
+ migrationBatchId,
186
+ source: 'engram',
187
+ [provenanceKey]: sourceProvenance ?? {},
188
+ });
189
+ const block = `\n\n<!-- vgxness-memory-import ${provenance} -->`;
190
+ return content.endsWith(block) ? content : `${content}${block}`;
191
+ }
192
+ function stripUndefined(value) {
193
+ return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined));
194
+ }
195
+ function identity(observation) {
196
+ return { project: observation.project, scope: observation.scope, type: observation.type, topicKey: observation.topicKey };
197
+ }
198
+ function artifactIdentity(artifact) {
199
+ return { project: artifact.project, change: artifact.change, phase: artifact.phase, topicKey: artifact.topicKey };
200
+ }
201
+ function stableJson(value) {
202
+ return JSON.stringify(sortJson(value));
203
+ }
204
+ function sortJson(value) {
205
+ if (Array.isArray(value))
206
+ return value.map(sortJson);
207
+ if (typeof value !== 'object' || value === null)
208
+ return value;
209
+ return Object.fromEntries(Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => [key, sortJson(item)]));
210
+ }
211
+ function slug(value) {
212
+ const normalized = value.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
213
+ return normalized.length > 0 ? normalized : 'untitled';
214
+ }
215
+ function formatValidationErrors(errors) {
216
+ return errors.map((error) => `${error.path}: ${error.message}`).join('; ');
217
+ }
218
+ function validationFailure(message) {
219
+ return { ok: false, error: { code: 'validation_failed', message } };
220
+ }
@@ -0,0 +1,178 @@
1
+ import { isSddPhase, sddPhases, sddTopicKey } from '../../sdd/schema.js';
2
+ export const MEMORY_IMPORT_PACKAGE_KIND = 'vgxness.memory-import-package';
3
+ export const MEMORY_IMPORT_PACKAGE_VERSION = 1;
4
+ export const MEMORY_IMPORT_PACKAGE_SOURCE = 'engram';
5
+ const scopes = ['project', 'personal'];
6
+ const provenanceStringFields = ['system', 'id', 'sourceId', 'createdAt', 'updatedAt', 'sessionId'];
7
+ export function validateMemoryImportPackage(input) {
8
+ const errors = [];
9
+ if (!isRecord(input)) {
10
+ return { ok: false, errors: [{ path: '$', message: 'package must be an object' }] };
11
+ }
12
+ requireExact(input, 'kind', MEMORY_IMPORT_PACKAGE_KIND, errors, 'unsupported package kind');
13
+ validateVersion(input.version, errors);
14
+ requireExact(input, 'source', MEMORY_IMPORT_PACKAGE_SOURCE, errors, 'unsupported package source');
15
+ requireNonEmptyString(input, 'exportedAt', errors);
16
+ requireNonEmptyString(input, 'migrationBatchId', errors);
17
+ if (input.observations === undefined) {
18
+ errors.push({ path: 'observations', message: 'observations is required' });
19
+ }
20
+ else if (!Array.isArray(input.observations)) {
21
+ errors.push({ path: 'observations', message: 'observations must be an array' });
22
+ }
23
+ else {
24
+ input.observations.forEach((item, index) => validateObservation(item, index, errors));
25
+ }
26
+ if (input.sddArtifacts !== undefined) {
27
+ if (!Array.isArray(input.sddArtifacts)) {
28
+ errors.push({ path: 'sddArtifacts', message: 'sddArtifacts must be an array when present' });
29
+ }
30
+ else {
31
+ input.sddArtifacts.forEach((item, index) => validateSddArtifact(item, index, errors));
32
+ }
33
+ }
34
+ if (input.sessionSummaries !== undefined) {
35
+ if (!Array.isArray(input.sessionSummaries)) {
36
+ errors.push({ path: 'sessionSummaries', message: 'sessionSummaries must be an array when present' });
37
+ }
38
+ else {
39
+ input.sessionSummaries.forEach((item, index) => validateSessionSummary(item, index, errors));
40
+ }
41
+ }
42
+ if (errors.length > 0)
43
+ return { ok: false, errors };
44
+ return { ok: true, package: input };
45
+ }
46
+ function validateObservation(input, index, errors) {
47
+ const path = `observations[${index}]`;
48
+ if (!isRecord(input)) {
49
+ errors.push({ path, index, message: 'observation must be an object' });
50
+ return;
51
+ }
52
+ requireNonEmptyString(input, 'project', errors, path, index);
53
+ requireOneOf(input, 'scope', scopes, errors, path, index);
54
+ requireNonEmptyString(input, 'type', errors, path, index);
55
+ requireNonEmptyString(input, 'title', errors, path, index);
56
+ requireNonEmptyString(input, 'content', errors, path, index);
57
+ optionalNonEmptyString(input, 'topicKey', errors, path, index);
58
+ validateOptionalProvenance(input, path, index, errors);
59
+ }
60
+ function validateSddArtifact(input, index, errors) {
61
+ const path = `sddArtifacts[${index}]`;
62
+ if (!isRecord(input)) {
63
+ errors.push({ path, index, message: 'SDD artifact must be an object' });
64
+ return;
65
+ }
66
+ requireNonEmptyString(input, 'project', errors, path, index);
67
+ requireNonEmptyString(input, 'change', errors, path, index);
68
+ const phase = requireSddPhase(input, errors, path, index);
69
+ requireNonEmptyString(input, 'content', errors, path, index);
70
+ optionalNonEmptyString(input, 'topicKey', errors, path, index);
71
+ validateOptionalProvenance(input, path, index, errors);
72
+ if (typeof input.topicKey === 'string' && typeof input.change === 'string' && phase !== undefined) {
73
+ const expected = sddTopicKey(input.change, phase);
74
+ if (input.topicKey !== expected) {
75
+ errors.push({ path: `${path}.topicKey`, index, message: `topicKey must match canonical SDD topic key ${expected}` });
76
+ }
77
+ }
78
+ }
79
+ function validateSessionSummary(input, index, errors) {
80
+ const path = `sessionSummaries[${index}]`;
81
+ if (!isRecord(input)) {
82
+ errors.push({ path, index, message: 'session summary must be an object' });
83
+ return;
84
+ }
85
+ requireNonEmptyString(input, 'content', errors, path, index);
86
+ optionalNonEmptyString(input, 'project', errors, path, index);
87
+ optionalNonEmptyString(input, 'type', errors, path, index);
88
+ optionalNonEmptyString(input, 'sourceSessionId', errors, path, index);
89
+ optionalNonEmptyString(input, 'sessionId', errors, path, index);
90
+ optionalNonEmptyString(input, 'topicKey', errors, path, index);
91
+ validateOptionalProvenance(input, path, index, errors);
92
+ }
93
+ function validateVersion(value, errors) {
94
+ if (value === undefined) {
95
+ errors.push({ path: 'version', message: 'version is required' });
96
+ return;
97
+ }
98
+ if (value !== MEMORY_IMPORT_PACKAGE_VERSION) {
99
+ errors.push({ path: 'version', message: `unsupported package version; expected version ${MEMORY_IMPORT_PACKAGE_VERSION}; received ${renderReceivedValue(value)}` });
100
+ }
101
+ }
102
+ function requireExact(record, field, expected, errors, message) {
103
+ if (record[field] === undefined) {
104
+ errors.push({ path: field, message: `${field} is required` });
105
+ return;
106
+ }
107
+ if (record[field] !== expected) {
108
+ errors.push({ path: field, message: `${message}; expected ${expected}; received ${renderReceivedValue(record[field])}` });
109
+ }
110
+ }
111
+ function renderReceivedValue(value) {
112
+ return typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean'
113
+ ? String(value)
114
+ : 'provided value';
115
+ }
116
+ function requireNonEmptyString(record, field, errors, parentPath, index) {
117
+ const path = parentPath === undefined ? field : `${parentPath}.${field}`;
118
+ if (record[field] === undefined) {
119
+ pushError(errors, path, `${field} is required`, index);
120
+ return undefined;
121
+ }
122
+ if (typeof record[field] !== 'string') {
123
+ pushError(errors, path, `${field} must be a string`, index);
124
+ return undefined;
125
+ }
126
+ if (record[field].trim().length === 0) {
127
+ pushError(errors, path, `${field} must not be empty`, index);
128
+ return undefined;
129
+ }
130
+ return record[field];
131
+ }
132
+ function optionalNonEmptyString(record, field, errors, parentPath, index) {
133
+ if (record[field] === undefined)
134
+ return;
135
+ requireNonEmptyString(record, field, errors, parentPath, index);
136
+ }
137
+ function requireOneOf(record, field, values, errors, parentPath, index) {
138
+ const value = requireNonEmptyString(record, field, errors, parentPath, index);
139
+ if (value === undefined)
140
+ return undefined;
141
+ if (!values.includes(value)) {
142
+ errors.push({ path: `${parentPath}.${field}`, index, message: `${field} must be one of: ${values.join(', ')}` });
143
+ return undefined;
144
+ }
145
+ return value;
146
+ }
147
+ function requireSddPhase(record, errors, parentPath, index) {
148
+ const value = requireNonEmptyString(record, 'phase', errors, parentPath, index);
149
+ if (value === undefined)
150
+ return undefined;
151
+ if (!isSddPhase(value)) {
152
+ errors.push({ path: `${parentPath}.phase`, index, message: `phase must be one of: ${sddPhases.join(', ')}` });
153
+ return undefined;
154
+ }
155
+ return value;
156
+ }
157
+ function validateOptionalProvenance(record, parentPath, index, errors) {
158
+ if (record.source === undefined)
159
+ return;
160
+ if (!isRecord(record.source)) {
161
+ errors.push({ path: `${parentPath}.source`, index, message: 'source must be an object when present' });
162
+ return;
163
+ }
164
+ for (const field of provenanceStringFields) {
165
+ if (record.source[field] !== undefined && typeof record.source[field] !== 'string') {
166
+ errors.push({ path: `${parentPath}.source.${field}`, index, message: `${field} must be a string when present` });
167
+ }
168
+ }
169
+ }
170
+ function isRecord(value) {
171
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
172
+ }
173
+ function pushError(errors, path, message, index) {
174
+ const error = { path, message };
175
+ if (index !== undefined)
176
+ error.index = index;
177
+ errors.push(error);
178
+ }
@@ -0,0 +1,126 @@
1
+ import { ArtifactRepository } from './repositories/artifacts.js';
2
+ import { ObservationRepository } from './repositories/observations.js';
3
+ import { SessionRepository } from './repositories/sessions.js';
4
+ import { TraceRepository } from './repositories/traces.js';
5
+ export class MemoryService {
6
+ database;
7
+ observations;
8
+ sessions;
9
+ artifacts;
10
+ traces;
11
+ constructor(database) {
12
+ this.database = database;
13
+ this.observations = new ObservationRepository(database);
14
+ this.sessions = new SessionRepository(database);
15
+ this.artifacts = new ArtifactRepository(database);
16
+ this.traces = new TraceRepository(database);
17
+ }
18
+ transaction(operation) {
19
+ let failure;
20
+ const result = this.database.transaction(() => {
21
+ const operationResult = operation();
22
+ if (!operationResult.ok) {
23
+ failure = operationResult;
24
+ throw new MemoryServiceTransactionRollback();
25
+ }
26
+ return operationResult.value;
27
+ });
28
+ if (!result.ok)
29
+ return failure ?? result;
30
+ return { ok: true, value: result.value };
31
+ }
32
+ saveObservation(input, context) {
33
+ const result = this.observations.save(input);
34
+ return this.record(result, context, { operation: input.topicKey ? 'observation.upsert' : 'observation.save', targetType: 'observation', targetId: result.ok ? result.value.id : undefined, topicKey: input.topicKey });
35
+ }
36
+ getObservation(id, context) {
37
+ const result = this.observations.getById(id);
38
+ return this.record(result, context, { operation: 'observation.get', targetType: 'observation', targetId: id });
39
+ }
40
+ updateObservation(input, context) {
41
+ const result = this.observations.update(input);
42
+ return this.record(result, context, { operation: 'observation.update', targetType: 'observation', targetId: input.id, topicKey: input.topicKey });
43
+ }
44
+ searchObservations(filters, context) {
45
+ const result = this.observations.searchPreviews(filters);
46
+ return this.record(result, context, { operation: 'observation.search', targetType: 'observation', topicKey: filters.topicKey });
47
+ }
48
+ listObservationRevisions(id, context) {
49
+ const result = this.observations.listRevisions(id);
50
+ return this.record(result, context, { operation: 'observation.revisions.list', targetType: 'observation', targetId: id });
51
+ }
52
+ saveSession(input, context) {
53
+ const result = this.sessions.save(input);
54
+ return this.record(result, context, { operation: 'session.save', targetType: 'session', targetId: result.ok ? result.value.id : input.id });
55
+ }
56
+ appendActivity(input, context) {
57
+ const result = this.sessions.appendActivity(input);
58
+ return this.record(result, context, { operation: 'session.activity.append', targetType: 'session_activity', targetId: result.ok ? result.value.id : input.sessionId });
59
+ }
60
+ closeSession(input, context) {
61
+ const result = this.sessions.closeSession(input);
62
+ return this.record(result, context, { operation: 'session.close', targetType: 'session', targetId: input.sessionId });
63
+ }
64
+ /** Read the latest closed session summary without writing provenance traces. */
65
+ restoreSession(input) {
66
+ return this.sessions.restoreLatest(input);
67
+ }
68
+ saveArtifact(input, context) {
69
+ const observation = this.saveObservation({ project: input.project, scope: 'project', type: 'sdd-artifact', title: input.topicKey, content: input.content, topicKey: input.topicKey }, context);
70
+ if (!observation.ok)
71
+ return observation;
72
+ const result = this.artifacts.save({ ...input, observationId: observation.value.id });
73
+ return this.record(result, context, { operation: 'artifact.save', targetType: 'artifact', targetId: result.ok ? result.value.id : undefined, topicKey: input.topicKey });
74
+ }
75
+ getArtifact(project, topicKey, context) {
76
+ const result = this.artifacts.getByTopic(project, topicKey);
77
+ return this.record(result, context, { operation: 'artifact.get', targetType: 'artifact', targetId: result.ok ? result.value.id : undefined, topicKey });
78
+ }
79
+ /**
80
+ * Read an artifact without writing a trace. This is only for read-only UI/status
81
+ * boundaries that must not mutate memory while checking local artifact state.
82
+ */
83
+ getArtifactNoTrace(project, topicKey) {
84
+ return this.artifacts.getByTopic(project, topicKey);
85
+ }
86
+ /**
87
+ * Check artifact presence without writing a trace. Missing artifacts are a
88
+ * successful `false` result so callers can distinguish absence from store errors.
89
+ */
90
+ hasArtifactNoTrace(project, topicKey) {
91
+ const artifact = this.getArtifactNoTrace(project, topicKey);
92
+ if (artifact.ok)
93
+ return { ok: true, value: true };
94
+ if (artifact.error.code === 'not_found')
95
+ return { ok: true, value: false };
96
+ return artifact;
97
+ }
98
+ listArtifactsByTopicPrefix(project, topicPrefix, context) {
99
+ const result = this.artifacts.listByTopicPrefix(project, topicPrefix);
100
+ return this.record(result, context, { operation: 'artifact.list', targetType: 'artifact', topicKey: topicPrefix });
101
+ }
102
+ close() { this.database.close(); }
103
+ record(result, context, parts) {
104
+ this.trace(context, { ...parts, status: result.ok ? 'ok' : 'error', error: result.ok ? undefined : result.error });
105
+ return result;
106
+ }
107
+ trace(context, parts) {
108
+ const input = { actor: context.actor, operation: parts.operation, targetType: parts.targetType, status: parts.status };
109
+ if (context.sessionId !== undefined)
110
+ input.sessionId = context.sessionId;
111
+ if (parts.targetId !== undefined)
112
+ input.targetId = parts.targetId;
113
+ if (parts.topicKey !== undefined)
114
+ input.topicKey = parts.topicKey;
115
+ if (parts.error !== undefined) {
116
+ input.errorCode = parts.error.code;
117
+ input.errorMessage = parts.error.message;
118
+ }
119
+ this.traces.write(input);
120
+ }
121
+ }
122
+ class MemoryServiceTransactionRollback extends Error {
123
+ constructor() {
124
+ super('Memory service transaction rolled back');
125
+ }
126
+ }
@@ -0,0 +1,41 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ export class ArtifactRepository {
3
+ db;
4
+ constructor(db) {
5
+ this.db = db;
6
+ }
7
+ save(input) {
8
+ const result = this.db.transaction(() => {
9
+ const existing = this.db.connection.prepare('SELECT * FROM artifacts WHERE project=@project AND topic_key=@topicKey').get(input);
10
+ const now = new Date().toISOString();
11
+ this.db.connection.prepare(`INSERT INTO artifacts(id, project, topic_key, phase, content, observation_id, created_at, updated_at)
12
+ VALUES (@id, @project, @topicKey, @phase, @content, @observationId, @createdAt, @updatedAt)
13
+ ON CONFLICT(project, topic_key) DO UPDATE SET phase=excluded.phase, content=excluded.content, observation_id=excluded.observation_id, updated_at=excluded.updated_at`).run({ ...input, id: existing?.id ?? randomUUID(), createdAt: existing?.created_at ?? now, updatedAt: now });
14
+ const artifact = this.getByTopic(input.project, input.topicKey);
15
+ if (!artifact.ok)
16
+ throw new Error(artifact.error.message);
17
+ return artifact.value;
18
+ });
19
+ return result.ok ? result : fail(result.error.message, result.error.cause);
20
+ }
21
+ getByTopic(project, topicKey) {
22
+ try {
23
+ const row = this.db.connection.prepare('SELECT * FROM artifacts WHERE project=? AND topic_key=?').get(project, topicKey);
24
+ return row ? { ok: true, value: map(row) } : { ok: false, error: { code: 'not_found', message: `Artifact not found: ${topicKey}` } };
25
+ }
26
+ catch (cause) {
27
+ return fail('Failed to read artifact', cause);
28
+ }
29
+ }
30
+ listByTopicPrefix(project, topicPrefix) {
31
+ try {
32
+ const rows = this.db.connection.prepare('SELECT * FROM artifacts WHERE project=? AND topic_key LIKE ? ORDER BY topic_key ASC').all(project, `${topicPrefix}%`);
33
+ return { ok: true, value: rows.map(map).filter((artifact) => artifact.topicKey.startsWith(topicPrefix)) };
34
+ }
35
+ catch (cause) {
36
+ return fail('Failed to list artifacts', cause);
37
+ }
38
+ }
39
+ }
40
+ function map(row) { return { id: row.id, project: row.project, topicKey: row.topic_key, phase: row.phase, content: row.content, observationId: row.observation_id, createdAt: row.created_at, updatedAt: row.updated_at }; }
41
+ function fail(message, cause) { return { ok: false, error: { code: 'validation_failed', message, cause } }; }