opencodekit 0.20.2 → 0.20.3
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/dist/index.js +1 -1
- package/dist/template/.opencode/agent/build.md +4 -0
- package/dist/template/.opencode/agent/explore.md +4 -0
- package/dist/template/.opencode/agent/general.md +4 -0
- package/dist/template/.opencode/agent/plan.md +4 -0
- package/dist/template/.opencode/agent/review.md +4 -0
- package/dist/template/.opencode/agent/scout.md +4 -0
- package/dist/template/.opencode/command/create.md +119 -25
- package/dist/template/.opencode/command/design.md +1 -2
- package/dist/template/.opencode/command/health.md +234 -0
- package/dist/template/.opencode/command/init-user.md +15 -0
- package/dist/template/.opencode/command/plan.md +3 -4
- package/dist/template/.opencode/command/pr.md +13 -0
- package/dist/template/.opencode/command/research.md +15 -3
- package/dist/template/.opencode/command/review-codebase.md +11 -1
- package/dist/template/.opencode/command/ship.md +72 -8
- package/dist/template/.opencode/command/status.md +1 -1
- package/dist/template/.opencode/command/ui-review.md +0 -1
- package/dist/template/.opencode/command/ui-slop-check.md +1 -1
- package/dist/template/.opencode/command/verify.md +11 -1
- package/dist/template/.opencode/memory.db +0 -0
- package/dist/template/.opencode/memory.db-shm +0 -0
- package/dist/template/.opencode/memory.db-wal +0 -0
- package/dist/template/.opencode/opencode.json +1678 -1677
- package/dist/template/.opencode/plugin/lib/compile.ts +253 -0
- package/dist/template/.opencode/plugin/lib/index-generator.ts +170 -0
- package/dist/template/.opencode/plugin/lib/lint.ts +359 -0
- package/dist/template/.opencode/plugin/lib/memory-admin-tools.ts +42 -1
- package/dist/template/.opencode/plugin/lib/memory-db.ts +7 -0
- package/dist/template/.opencode/plugin/lib/memory-helpers.ts +30 -0
- package/dist/template/.opencode/plugin/lib/memory-hooks.ts +10 -0
- package/dist/template/.opencode/plugin/lib/memory-tools.ts +30 -1
- package/dist/template/.opencode/plugin/lib/operation-log.ts +109 -0
- package/dist/template/.opencode/plugin/lib/validate.ts +243 -0
- package/dist/template/.opencode/skill/design-taste-frontend/SKILL.md +13 -1
- package/dist/template/.opencode/skill/figma-go/SKILL.md +1 -1
- package/dist/template/.opencode/skill/full-output-enforcement/SKILL.md +13 -0
- package/dist/template/.opencode/skill/high-end-visual-design/SKILL.md +13 -0
- package/dist/template/.opencode/skill/industrial-brutalist-ui/SKILL.md +13 -0
- package/dist/template/.opencode/skill/memory-system/SKILL.md +65 -1
- package/dist/template/.opencode/skill/minimalist-ui/SKILL.md +13 -0
- package/dist/template/.opencode/skill/redesign-existing-projects/SKILL.md +13 -0
- package/dist/template/.opencode/skill/requesting-code-review/SKILL.md +48 -2
- package/dist/template/.opencode/skill/requesting-code-review/references/specialist-profiles.md +108 -0
- package/dist/template/.opencode/skill/skill-creator/SKILL.md +25 -0
- package/dist/template/.opencode/skill/stitch-design-taste/SKILL.md +13 -0
- package/dist/template/.opencode/skill/verification-before-completion/SKILL.md +46 -0
- package/package.json +1 -1
- package/dist/template/.opencode/agent/runner.md +0 -79
- package/dist/template/.opencode/command/start.md +0 -156
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory Compilation — Observations → Structured Articles
|
|
3
|
+
*
|
|
4
|
+
* Inspired by Karpathy's LLM Wiki "compilation" layer:
|
|
5
|
+
* reads clusters of related observations and produces structured
|
|
6
|
+
* summary articles with cross-references. This is the missing 5th stage:
|
|
7
|
+
*
|
|
8
|
+
* capture → distill → curate → **compile** → inject
|
|
9
|
+
*
|
|
10
|
+
* Unlike Karpathy's approach (which uses LLM for compilation), this
|
|
11
|
+
* implementation uses pure heuristics — grouping by concept clusters,
|
|
12
|
+
* ranking by confidence/recency, and templated article generation.
|
|
13
|
+
*
|
|
14
|
+
* For LLM-powered compilation, use the compile admin operation which
|
|
15
|
+
* generates articles the agent can review and edit.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { upsertMemoryFile } from "./db/maintenance.js";
|
|
19
|
+
import type { ObservationRow } from "./db/types.js";
|
|
20
|
+
import { getMemoryDB } from "./memory-db.js";
|
|
21
|
+
import { TYPE_ICONS, parseConcepts } from "./memory-helpers.js";
|
|
22
|
+
import { appendOperationLog } from "./operation-log.js";
|
|
23
|
+
|
|
24
|
+
// ============================================================================
|
|
25
|
+
// Types
|
|
26
|
+
// ============================================================================
|
|
27
|
+
|
|
28
|
+
export interface ConceptCluster {
|
|
29
|
+
concept: string;
|
|
30
|
+
observations: Pick<
|
|
31
|
+
ObservationRow,
|
|
32
|
+
"id" | "type" | "title" | "narrative" | "confidence" | "created_at"
|
|
33
|
+
>[];
|
|
34
|
+
relatedConcepts: string[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface CompiledArticle {
|
|
38
|
+
concept: string;
|
|
39
|
+
content: string;
|
|
40
|
+
observationCount: number;
|
|
41
|
+
relatedConcepts: string[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface CompileResult {
|
|
45
|
+
articles: CompiledArticle[];
|
|
46
|
+
totalObservations: number;
|
|
47
|
+
skippedClusters: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ============================================================================
|
|
51
|
+
// Compilation Operations
|
|
52
|
+
// ============================================================================
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Compile observations into structured articles grouped by concept.
|
|
56
|
+
* Only generates articles for concepts with `minObservations` or more observations.
|
|
57
|
+
* Stores articles in memory_files as "compiled/{concept}".
|
|
58
|
+
*/
|
|
59
|
+
export function compileObservations(
|
|
60
|
+
options: { minObservations?: number; maxArticles?: number } = {},
|
|
61
|
+
): CompileResult {
|
|
62
|
+
const minObs = options.minObservations ?? 3;
|
|
63
|
+
const maxArticles = options.maxArticles ?? 20;
|
|
64
|
+
|
|
65
|
+
const clusters = buildConceptClusters(minObs);
|
|
66
|
+
const articles: CompiledArticle[] = [];
|
|
67
|
+
let skipped = 0;
|
|
68
|
+
|
|
69
|
+
// Sort clusters by observation count (most connected first)
|
|
70
|
+
const sortedClusters = clusters
|
|
71
|
+
.sort((a, b) => b.observations.length - a.observations.length)
|
|
72
|
+
.slice(0, maxArticles);
|
|
73
|
+
|
|
74
|
+
let totalObservations = 0;
|
|
75
|
+
|
|
76
|
+
for (const cluster of sortedClusters) {
|
|
77
|
+
const article = compileCluster(cluster);
|
|
78
|
+
if (article) {
|
|
79
|
+
articles.push(article);
|
|
80
|
+
totalObservations += article.observationCount;
|
|
81
|
+
// Store in memory_files
|
|
82
|
+
const safeName = cluster.concept.replace(/[^a-z0-9-]/g, "-");
|
|
83
|
+
upsertMemoryFile(`compiled/${safeName}`, article.content, "replace");
|
|
84
|
+
} else {
|
|
85
|
+
skipped++;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Log the operation
|
|
90
|
+
appendOperationLog({
|
|
91
|
+
operation: "compile-run",
|
|
92
|
+
targets: articles.map((a) => a.concept),
|
|
93
|
+
summary: `Compiled ${articles.length} articles from ${totalObservations} observations (${skipped} clusters skipped)`,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
return { articles, totalObservations, skippedClusters: skipped };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ============================================================================
|
|
100
|
+
// Internal
|
|
101
|
+
// ============================================================================
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Build concept clusters from active observations.
|
|
105
|
+
*/
|
|
106
|
+
function buildConceptClusters(minObservations: number): ConceptCluster[] {
|
|
107
|
+
const db = getMemoryDB();
|
|
108
|
+
|
|
109
|
+
const observations = db
|
|
110
|
+
.query(
|
|
111
|
+
`SELECT id, type, title, narrative, concepts, confidence, created_at
|
|
112
|
+
FROM observations
|
|
113
|
+
WHERE superseded_by IS NULL AND concepts IS NOT NULL
|
|
114
|
+
ORDER BY created_at_epoch DESC`,
|
|
115
|
+
)
|
|
116
|
+
.all() as Pick<
|
|
117
|
+
ObservationRow,
|
|
118
|
+
| "id"
|
|
119
|
+
| "type"
|
|
120
|
+
| "title"
|
|
121
|
+
| "narrative"
|
|
122
|
+
| "concepts"
|
|
123
|
+
| "confidence"
|
|
124
|
+
| "created_at"
|
|
125
|
+
>[];
|
|
126
|
+
|
|
127
|
+
// Build concept → observations map
|
|
128
|
+
const conceptMap = new Map<string, typeof observations>();
|
|
129
|
+
|
|
130
|
+
for (const obs of observations) {
|
|
131
|
+
const concepts = parseConcepts(obs.concepts);
|
|
132
|
+
for (const concept of concepts) {
|
|
133
|
+
const group = conceptMap.get(concept) ?? [];
|
|
134
|
+
group.push(obs);
|
|
135
|
+
conceptMap.set(concept, group);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Filter to clusters with enough observations
|
|
140
|
+
const clusters: ConceptCluster[] = [];
|
|
141
|
+
for (const [concept, obsGroup] of conceptMap) {
|
|
142
|
+
if (obsGroup.length < minObservations) continue;
|
|
143
|
+
|
|
144
|
+
// Find related concepts (co-occurring)
|
|
145
|
+
const relatedCounts = new Map<string, number>();
|
|
146
|
+
for (const obs of obsGroup) {
|
|
147
|
+
const concepts = parseConcepts(obs.concepts);
|
|
148
|
+
for (const c of concepts) {
|
|
149
|
+
if (c === concept) continue;
|
|
150
|
+
relatedCounts.set(c, (relatedCounts.get(c) ?? 0) + 1);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const relatedConcepts = [...relatedCounts.entries()]
|
|
154
|
+
.filter(([, count]) => count >= 2)
|
|
155
|
+
.sort((a, b) => b[1] - a[1])
|
|
156
|
+
.map(([c]) => c);
|
|
157
|
+
|
|
158
|
+
clusters.push({
|
|
159
|
+
concept,
|
|
160
|
+
observations: obsGroup,
|
|
161
|
+
relatedConcepts,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return clusters;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Compile a single concept cluster into a markdown article.
|
|
170
|
+
*/
|
|
171
|
+
function compileCluster(cluster: ConceptCluster): CompiledArticle | null {
|
|
172
|
+
if (cluster.observations.length === 0) return null;
|
|
173
|
+
|
|
174
|
+
const lines: string[] = [];
|
|
175
|
+
|
|
176
|
+
// Header
|
|
177
|
+
lines.push(`# ${cluster.concept}`);
|
|
178
|
+
lines.push("");
|
|
179
|
+
lines.push(`> Compiled from ${cluster.observations.length} observations.`);
|
|
180
|
+
lines.push(`> Last compiled: ${new Date().toISOString().slice(0, 19)}`);
|
|
181
|
+
|
|
182
|
+
// Related concepts
|
|
183
|
+
if (cluster.relatedConcepts.length > 0) {
|
|
184
|
+
lines.push(`> Related: ${cluster.relatedConcepts.join(", ")}`);
|
|
185
|
+
}
|
|
186
|
+
lines.push("");
|
|
187
|
+
|
|
188
|
+
// Group observations by type for structure
|
|
189
|
+
const byType = new Map<string, typeof cluster.observations>();
|
|
190
|
+
for (const obs of cluster.observations) {
|
|
191
|
+
const group = byType.get(obs.type) ?? [];
|
|
192
|
+
group.push(obs);
|
|
193
|
+
byType.set(obs.type, group);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Decisions first (most important)
|
|
197
|
+
const typeOrder = [
|
|
198
|
+
"decision",
|
|
199
|
+
"pattern",
|
|
200
|
+
"warning",
|
|
201
|
+
"bugfix",
|
|
202
|
+
"discovery",
|
|
203
|
+
"feature",
|
|
204
|
+
"learning",
|
|
205
|
+
];
|
|
206
|
+
for (const type of typeOrder) {
|
|
207
|
+
const group = byType.get(type);
|
|
208
|
+
if (!group || group.length === 0) continue;
|
|
209
|
+
|
|
210
|
+
const icon = TYPE_ICONS[type] ?? "📌";
|
|
211
|
+
const heading = type.charAt(0).toUpperCase() + type.slice(1);
|
|
212
|
+
lines.push(`## ${icon} ${heading}s`);
|
|
213
|
+
lines.push("");
|
|
214
|
+
|
|
215
|
+
for (const obs of group) {
|
|
216
|
+
lines.push(`### #${obs.id}: ${obs.title}`);
|
|
217
|
+
if (obs.narrative) {
|
|
218
|
+
// Truncate long narratives for the compiled view
|
|
219
|
+
const narrative =
|
|
220
|
+
obs.narrative.length > 500
|
|
221
|
+
? `${obs.narrative.slice(0, 500)}...`
|
|
222
|
+
: obs.narrative;
|
|
223
|
+
lines.push("");
|
|
224
|
+
lines.push(narrative);
|
|
225
|
+
}
|
|
226
|
+
lines.push("");
|
|
227
|
+
lines.push(
|
|
228
|
+
`_Confidence: ${obs.confidence} | Created: ${obs.created_at.slice(0, 10)}_`,
|
|
229
|
+
);
|
|
230
|
+
lines.push("");
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Cross-reference footer
|
|
235
|
+
lines.push("---");
|
|
236
|
+
lines.push("");
|
|
237
|
+
lines.push(
|
|
238
|
+
`**Source observations:** ${cluster.observations.map((o) => `#${o.id}`).join(", ")}`,
|
|
239
|
+
);
|
|
240
|
+
if (cluster.relatedConcepts.length > 0) {
|
|
241
|
+
lines.push(
|
|
242
|
+
`**See also:** ${cluster.relatedConcepts.map((c) => `[[${c}]]`).join(", ")}`,
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return {
|
|
247
|
+
concept: cluster.concept,
|
|
248
|
+
content: lines.join("\n"),
|
|
249
|
+
observationCount: cluster.observations.length,
|
|
250
|
+
relatedConcepts: cluster.relatedConcepts,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory Index Generator — Auto-Generated Knowledge Catalog
|
|
3
|
+
*
|
|
4
|
+
* Inspired by Karpathy's LLM Wiki index.md:
|
|
5
|
+
* generates a structured catalog of all observations, grouped by type,
|
|
6
|
+
* with cross-references and concept clusters.
|
|
7
|
+
*
|
|
8
|
+
* Stored in memory_files as "index" for injection or on-demand reading.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { upsertMemoryFile } from "./db/maintenance.js";
|
|
12
|
+
import type { ObservationRow } from "./db/types.js";
|
|
13
|
+
import { getMemoryDB } from "./memory-db.js";
|
|
14
|
+
import { TYPE_ICONS, parseConcepts } from "./memory-helpers.js";
|
|
15
|
+
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// Types
|
|
18
|
+
// ============================================================================
|
|
19
|
+
|
|
20
|
+
export interface IndexEntry {
|
|
21
|
+
id: number;
|
|
22
|
+
type: string;
|
|
23
|
+
title: string;
|
|
24
|
+
concepts: string[];
|
|
25
|
+
created_at: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface IndexResult {
|
|
29
|
+
entryCount: number;
|
|
30
|
+
conceptCount: number;
|
|
31
|
+
content: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ============================================================================
|
|
35
|
+
// Index Generation
|
|
36
|
+
// ============================================================================
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Generate a comprehensive index of all active observations.
|
|
40
|
+
* Groups by type, lists concept clusters, and writes to memory_files.
|
|
41
|
+
*/
|
|
42
|
+
export function generateMemoryIndex(): IndexResult {
|
|
43
|
+
const db = getMemoryDB();
|
|
44
|
+
|
|
45
|
+
const observations = db
|
|
46
|
+
.query(
|
|
47
|
+
`SELECT id, type, title, concepts, created_at FROM observations
|
|
48
|
+
WHERE superseded_by IS NULL
|
|
49
|
+
ORDER BY type, created_at_epoch DESC`,
|
|
50
|
+
)
|
|
51
|
+
.all() as Pick<
|
|
52
|
+
ObservationRow,
|
|
53
|
+
"id" | "type" | "title" | "concepts" | "created_at"
|
|
54
|
+
>[];
|
|
55
|
+
|
|
56
|
+
// Group by type
|
|
57
|
+
const byType = new Map<string, IndexEntry[]>();
|
|
58
|
+
const allConcepts = new Map<string, number[]>();
|
|
59
|
+
|
|
60
|
+
for (const obs of observations) {
|
|
61
|
+
const entry: IndexEntry = {
|
|
62
|
+
id: obs.id,
|
|
63
|
+
type: obs.type,
|
|
64
|
+
title: obs.title,
|
|
65
|
+
concepts: parseConcepts(obs.concepts),
|
|
66
|
+
created_at: obs.created_at,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const group = byType.get(obs.type) ?? [];
|
|
70
|
+
group.push(entry);
|
|
71
|
+
byType.set(obs.type, group);
|
|
72
|
+
|
|
73
|
+
for (const concept of entry.concepts) {
|
|
74
|
+
const ids = allConcepts.get(concept) ?? [];
|
|
75
|
+
ids.push(obs.id);
|
|
76
|
+
allConcepts.set(concept, ids);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Build markdown
|
|
81
|
+
const lines: string[] = [];
|
|
82
|
+
lines.push("# Memory Index");
|
|
83
|
+
lines.push("");
|
|
84
|
+
lines.push(
|
|
85
|
+
`> Auto-generated catalog of ${observations.length} active observations.`,
|
|
86
|
+
);
|
|
87
|
+
lines.push(`> Last updated: ${new Date().toISOString().slice(0, 19)}`);
|
|
88
|
+
lines.push("");
|
|
89
|
+
|
|
90
|
+
// Summary table
|
|
91
|
+
lines.push("## Summary");
|
|
92
|
+
lines.push("");
|
|
93
|
+
lines.push("| Type | Count |");
|
|
94
|
+
lines.push("|------|-------|");
|
|
95
|
+
for (const [type, entries] of byType) {
|
|
96
|
+
const icon = TYPE_ICONS[type] ?? "📌";
|
|
97
|
+
lines.push(`| ${icon} ${type} | ${entries.length} |`);
|
|
98
|
+
}
|
|
99
|
+
lines.push(`| **Total** | **${observations.length}** |`);
|
|
100
|
+
lines.push("");
|
|
101
|
+
|
|
102
|
+
// By type
|
|
103
|
+
for (const [type, entries] of byType) {
|
|
104
|
+
const icon = TYPE_ICONS[type] ?? "📌";
|
|
105
|
+
lines.push(
|
|
106
|
+
`## ${icon} ${type.charAt(0).toUpperCase() + type.slice(1)} (${entries.length})`,
|
|
107
|
+
);
|
|
108
|
+
lines.push("");
|
|
109
|
+
for (const entry of entries) {
|
|
110
|
+
const concepts =
|
|
111
|
+
entry.concepts.length > 0 ? ` [${entry.concepts.join(", ")}]` : "";
|
|
112
|
+
lines.push(
|
|
113
|
+
`- **#${entry.id}** ${entry.title}${concepts} _(${entry.created_at.slice(0, 10)})_`,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
lines.push("");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Concept clusters
|
|
120
|
+
const significantConcepts = [...allConcepts.entries()]
|
|
121
|
+
.filter(([, ids]) => ids.length >= 2)
|
|
122
|
+
.sort((a, b) => b[1].length - a[1].length);
|
|
123
|
+
|
|
124
|
+
if (significantConcepts.length > 0) {
|
|
125
|
+
lines.push("## Concept Clusters");
|
|
126
|
+
lines.push("");
|
|
127
|
+
lines.push("Concepts appearing in 2+ observations:");
|
|
128
|
+
lines.push("");
|
|
129
|
+
for (const [concept, ids] of significantConcepts.slice(0, 30)) {
|
|
130
|
+
lines.push(
|
|
131
|
+
`- **${concept}** (${ids.length}): ${ids.map((id) => `#${id}`).join(", ")}`,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
lines.push("");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Orphan concepts
|
|
138
|
+
const orphanConcepts = [...allConcepts.entries()].filter(
|
|
139
|
+
([, ids]) => ids.length === 1,
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
if (orphanConcepts.length > 0) {
|
|
143
|
+
lines.push("## Orphan Concepts");
|
|
144
|
+
lines.push("");
|
|
145
|
+
lines.push(
|
|
146
|
+
`${orphanConcepts.length} concepts appear in only 1 observation:`,
|
|
147
|
+
);
|
|
148
|
+
lines.push("");
|
|
149
|
+
const orphanList = orphanConcepts
|
|
150
|
+
.slice(0, 20)
|
|
151
|
+
.map(([concept, ids]) => `${concept} (#${ids[0]})`);
|
|
152
|
+
lines.push(orphanList.join(", "));
|
|
153
|
+
if (orphanConcepts.length > 20) {
|
|
154
|
+
lines.push(`... and ${orphanConcepts.length - 20} more`);
|
|
155
|
+
}
|
|
156
|
+
lines.push("");
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const content = lines.join("\n");
|
|
160
|
+
|
|
161
|
+
// Store in memory_files
|
|
162
|
+
upsertMemoryFile("index", content, "replace");
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
entryCount: observations.length,
|
|
166
|
+
conceptCount: allConcepts.size,
|
|
167
|
+
content,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|