@isparling/engram-coach 0.1.0 → 0.2.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 +85 -18
- package/SETUP.md +559 -0
- package/SKILL_PACK.md +75 -0
- package/analyses/catalog.md +257 -0
- package/analysis-tools/hrv-trend.ts +592 -0
- package/analysis-tools/migrate-structured-capture.ts +234 -0
- package/analysis-tools/race-context.ts +96 -0
- package/analysis-tools/stream-analyze.ts +1008 -0
- package/analysis-tools/tsb-predict.ts +117 -0
- package/capture-handler.ts +301 -0
- package/config.json.example +21 -0
- package/engram-coach-ambient-capture.ts +336 -0
- package/engram-coach-capture-types.ts +185 -0
- package/engram-coach-config.ts +268 -0
- package/engram-coach-domain.ts +7 -2
- package/engram-coach-keys.ts +189 -0
- package/engram-coach-materialization.ts +638 -0
- package/engram-coach-migration.ts +1078 -0
- package/engram-coach-pack.ts +17 -12
- package/engram-coach-presentation.ts +10 -1
- package/engram-coach-reconciliation.ts +305 -2
- package/engram-coach-structured-capture.ts +622 -0
- package/package.json +39 -6
- package/personas/aggressive-monitoring.md +121 -0
- package/personas/aggressive.json +85 -0
- package/personas/conservative-monitoring.md +133 -0
- package/personas/conservative.json +93 -0
- package/personas/polarized-monitoring.md +112 -0
- package/personas/polarized.json +72 -0
- package/personas/volume-monitoring.md +85 -0
- package/personas/volume.json +108 -0
- package/shared/retrieval.md +71 -0
- package/shared/setup.md +207 -0
- package/skills/.gitkeep +0 -0
- package/skills/adapt-plan/SKILL.md +263 -0
- package/skills/block-review/SKILL.md +275 -0
- package/skills/consult/SKILL.md +176 -0
- package/skills/intake/SKILL.md +315 -0
- package/skills/lactate-analyze/SKILL.md +230 -0
- package/skills/lessons-rollup/SKILL.md +196 -0
- package/skills/monitoring-rollup/SKILL.md +208 -0
- package/skills/race-analysis/SKILL.md +219 -0
- package/skills/season-retrospective/SKILL.md +200 -0
- package/skills/set-goal/SKILL.md +297 -0
- package/templates/base.md +55 -0
- package/templates/build-1.md +57 -0
- package/templates/build-2.md +62 -0
- package/templates/race-report.md +51 -0
- package/templates/race-specificity.md +62 -0
- package/templates/season-review.md +40 -0
- package/engram-coach-extractor.ts +0 -295
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* engram-coach ambient extraction — the LLM-only capture channel.
|
|
3
|
+
*
|
|
4
|
+
* At awaited OMP `session_stop` the extension hands this pack the latest user
|
|
5
|
+
* turn and a bounded, isolated `complete()` mechanic backed by a child OMP
|
|
6
|
+
* process. This module owns the prompt, the strict response schema, response
|
|
7
|
+
* validation, and the single repair attempt. Persistence, key derivation, and
|
|
8
|
+
* duplicate suppression live in `capture-handler.ts`.
|
|
9
|
+
*
|
|
10
|
+
* Two rules shape everything here:
|
|
11
|
+
*
|
|
12
|
+
* 1. Strict JSON only. Markdown fences are rejected rather than stripped: a
|
|
13
|
+
* fenced reply means the model ignored the contract, and quietly
|
|
14
|
+
* repairing it here would hide that from the one repair attempt.
|
|
15
|
+
* 2. No deterministic fallback. A timeout, a cancellation, a model failure,
|
|
16
|
+
* or a second invalid response yields a visible warning and no
|
|
17
|
+
* candidates — never a keyword-derived transcript excerpt.
|
|
18
|
+
*
|
|
19
|
+
* See `engram-coach-structured-capture.ts` for the explicit, hash-bound
|
|
20
|
+
* channel that skills use for facts they already structured.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import type {
|
|
24
|
+
JsonObject,
|
|
25
|
+
JsonValue,
|
|
26
|
+
KnowledgeKind,
|
|
27
|
+
} from "@isparling/engram-harness/knowledge-types";
|
|
28
|
+
import type { CompletionRequest } from "@isparling/engram-harness/capture-types";
|
|
29
|
+
import type { EngramCoachRuntimeConfig } from "./engram-coach-config.ts";
|
|
30
|
+
import type { KeyedEntityType } from "./engram-coach-keys.ts";
|
|
31
|
+
import { ENGRAM_COACH_ENTITY_TYPES } from "./engram-coach-domain.ts";
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Warning vocabulary
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
export const AMBIENT_INVALID_JSON_WARNING =
|
|
38
|
+
"ambient capture returned invalid JSON after one repair attempt";
|
|
39
|
+
export const AMBIENT_CANCELLED_WARNING = "ambient capture was cancelled before completion";
|
|
40
|
+
export const AMBIENT_TIMEOUT_WARNING = "ambient capture timed out before completion";
|
|
41
|
+
export const AMBIENT_MODEL_FAILED_WARNING = "ambient capture model call failed";
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Map a host completion failure onto one stable warning. The host
|
|
45
|
+
* distinguishes cancellation, timeout, and model failure by message prefix;
|
|
46
|
+
* anything else is reported as a model failure with its detail attached.
|
|
47
|
+
*/
|
|
48
|
+
export function completionWarning(error: unknown): string {
|
|
49
|
+
const message = String(error);
|
|
50
|
+
if (message.includes("capture_cancelled")) return AMBIENT_CANCELLED_WARNING;
|
|
51
|
+
if (message.includes("capture_timeout")) return AMBIENT_TIMEOUT_WARNING;
|
|
52
|
+
if (message.includes("capture_model_failed")) return AMBIENT_MODEL_FAILED_WARNING;
|
|
53
|
+
return `${AMBIENT_MODEL_FAILED_WARNING}: ${message}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// Response contract
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
const KNOWLEDGE_KINDS: readonly KnowledgeKind[] = [
|
|
61
|
+
"evidence",
|
|
62
|
+
"claim",
|
|
63
|
+
"interpretation",
|
|
64
|
+
"decision",
|
|
65
|
+
"recommendation",
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
/** State entity types that can carry a canonical key. */
|
|
69
|
+
const KEYED_ENTITY_TYPES: readonly KeyedEntityType[] = [
|
|
70
|
+
"workout",
|
|
71
|
+
"prescription",
|
|
72
|
+
"threshold",
|
|
73
|
+
"persona",
|
|
74
|
+
"monitoring",
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
export const MAX_STATEMENT_LENGTH = 512;
|
|
78
|
+
|
|
79
|
+
/** One validated ambient candidate, before it becomes an envelope. */
|
|
80
|
+
export type AmbientCandidate = {
|
|
81
|
+
kind: KnowledgeKind;
|
|
82
|
+
statement: string;
|
|
83
|
+
entityType: string | null;
|
|
84
|
+
keyedEntityType: KeyedEntityType | null;
|
|
85
|
+
keyComponents: JsonObject;
|
|
86
|
+
effectiveAt: string | null;
|
|
87
|
+
subjects: string[];
|
|
88
|
+
topics: string[];
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/** Host mechanic this module needs: one bounded, isolated completion. */
|
|
92
|
+
export type AmbientCompleter = {
|
|
93
|
+
complete(request: CompletionRequest): Promise<string>;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const RESPONSE_SCHEMA = `{
|
|
97
|
+
"candidates": [
|
|
98
|
+
{
|
|
99
|
+
"kind": "evidence | claim | interpretation | decision | recommendation",
|
|
100
|
+
"statement": "one atomic sentence, at most ${MAX_STATEMENT_LENGTH} characters",
|
|
101
|
+
"entity_type": "${ENGRAM_COACH_ENTITY_TYPES.join(" | ")}",
|
|
102
|
+
"keyed_entity_type": "${KEYED_ENTITY_TYPES.join(" | ")} | null",
|
|
103
|
+
"key_components": { "session_id": "...", "arc_id": "...", "sport": "...", "concern_id": "...", "signal": "..." },
|
|
104
|
+
"effective_at": "YYYY-MM-DD or null",
|
|
105
|
+
"subjects": ["..."],
|
|
106
|
+
"topics": ["..."]
|
|
107
|
+
}
|
|
108
|
+
]
|
|
109
|
+
}`;
|
|
110
|
+
|
|
111
|
+
const SYSTEM_PROMPT = [
|
|
112
|
+
"You extract durable coaching knowledge from one athlete message.",
|
|
113
|
+
"You reply with a single JSON object and nothing else.",
|
|
114
|
+
"You never wrap the JSON in Markdown fences, prose, or explanation.",
|
|
115
|
+
"You never invent facts that the message does not state or clearly imply.",
|
|
116
|
+
].join(" ");
|
|
117
|
+
|
|
118
|
+
export function extractionPrompt(
|
|
119
|
+
config: EngramCoachRuntimeConfig,
|
|
120
|
+
userText: string,
|
|
121
|
+
maxCandidates: number,
|
|
122
|
+
): string {
|
|
123
|
+
return [
|
|
124
|
+
"Extract durable coaching knowledge from the athlete's latest message.",
|
|
125
|
+
"",
|
|
126
|
+
`Active athlete profile: ${config.activeProfile}`,
|
|
127
|
+
`Return at most ${maxCandidates} candidate(s). Return zero candidates when the message carries no durable coaching fact.`,
|
|
128
|
+
"",
|
|
129
|
+
"Each statement must be one atomic, self-contained sentence — not a transcript excerpt.",
|
|
130
|
+
"Set `keyed_entity_type` and `key_components` only when the message names the entity unambiguously; otherwise use null and an empty object.",
|
|
131
|
+
"",
|
|
132
|
+
"Respond with exactly this JSON shape:",
|
|
133
|
+
RESPONSE_SCHEMA,
|
|
134
|
+
"",
|
|
135
|
+
"Athlete message:",
|
|
136
|
+
userText,
|
|
137
|
+
].join("\n");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function repairPrompt(original: string, errors: readonly string[]): string {
|
|
141
|
+
return [
|
|
142
|
+
"Your previous response was not valid for the required schema.",
|
|
143
|
+
"",
|
|
144
|
+
"Validation errors:",
|
|
145
|
+
...errors.map((error) => `- ${error}`),
|
|
146
|
+
"",
|
|
147
|
+
"Your previous response:",
|
|
148
|
+
original,
|
|
149
|
+
"",
|
|
150
|
+
"Reply again with a single valid JSON object matching exactly this shape, and nothing else:",
|
|
151
|
+
RESPONSE_SCHEMA,
|
|
152
|
+
].join("\n");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
// Strict parsing and validation
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
|
|
159
|
+
function isObject(value: unknown): value is Record<string, unknown> {
|
|
160
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function isJsonValue(value: unknown): value is JsonValue {
|
|
164
|
+
if (value === null) return true;
|
|
165
|
+
const type = typeof value;
|
|
166
|
+
if (type === "string" || type === "number" || type === "boolean") return true;
|
|
167
|
+
if (Array.isArray(value)) return value.every(isJsonValue);
|
|
168
|
+
return isObject(value) && Object.values(value).every(isJsonValue);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function jsonObject(value: unknown): JsonObject {
|
|
172
|
+
if (!isObject(value)) return {};
|
|
173
|
+
const result: JsonObject = {};
|
|
174
|
+
for (const [key, item] of Object.entries(value)) {
|
|
175
|
+
if (isJsonValue(item)) result[key] = item;
|
|
176
|
+
}
|
|
177
|
+
return result;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function stringArray(value: unknown): string[] {
|
|
181
|
+
return Array.isArray(value)
|
|
182
|
+
? value.filter((item): item is string => typeof item === "string")
|
|
183
|
+
: [];
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* One-line, length-capped statement. Ambient statements are atomic facts, so
|
|
188
|
+
* embedded newlines are collapsed rather than preserved.
|
|
189
|
+
*/
|
|
190
|
+
function normalizeStatement(value: string): string {
|
|
191
|
+
return value.trim().replace(/\s*\r?\n+\s*/g, " ").slice(0, MAX_STATEMENT_LENGTH);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export type AmbientParseOutcome =
|
|
195
|
+
| { ok: true; candidates: AmbientCandidate[] }
|
|
196
|
+
| { ok: false; errors: string[] };
|
|
197
|
+
|
|
198
|
+
/** Parse and validate one raw model response against the strict schema. */
|
|
199
|
+
export function parseAmbientResponse(raw: string, maxCandidates: number): AmbientParseOutcome {
|
|
200
|
+
const text = raw.trim();
|
|
201
|
+
if (text.length === 0) return { ok: false, errors: ["response was empty"] };
|
|
202
|
+
if (text.startsWith("```")) {
|
|
203
|
+
return { ok: false, errors: ["response was wrapped in a Markdown fence; reply with bare JSON"] };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
let parsed: unknown;
|
|
207
|
+
try {
|
|
208
|
+
parsed = JSON.parse(text);
|
|
209
|
+
} catch (error) {
|
|
210
|
+
return { ok: false, errors: [`response was not valid JSON: ${String(error)}`] };
|
|
211
|
+
}
|
|
212
|
+
if (!isObject(parsed)) {
|
|
213
|
+
return { ok: false, errors: ["top-level value must be a JSON object"] };
|
|
214
|
+
}
|
|
215
|
+
const rawCandidates = parsed.candidates;
|
|
216
|
+
if (!Array.isArray(rawCandidates)) {
|
|
217
|
+
return { ok: false, errors: ['"candidates" must be an array'] };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const errors: string[] = [];
|
|
221
|
+
const candidates: AmbientCandidate[] = [];
|
|
222
|
+
for (const [index, entry] of rawCandidates.entries()) {
|
|
223
|
+
if (!isObject(entry)) {
|
|
224
|
+
errors.push(`candidates[${index}] must be an object`);
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
const rawStatement = entry.statement;
|
|
228
|
+
if (typeof rawStatement !== "string" || rawStatement.trim().length === 0) {
|
|
229
|
+
errors.push(`candidates[${index}].statement must be a nonempty string`);
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
const kind = entry.kind;
|
|
233
|
+
if (typeof kind !== "string" || !(KNOWLEDGE_KINDS as readonly string[]).includes(kind)) {
|
|
234
|
+
errors.push(`candidates[${index}].kind must be one of ${KNOWLEDGE_KINDS.join(", ")}`);
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
const rawEntityType = entry.entity_type;
|
|
238
|
+
if (rawEntityType !== undefined && rawEntityType !== null && typeof rawEntityType !== "string") {
|
|
239
|
+
errors.push(`candidates[${index}].entity_type must be a string or null`);
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
if (
|
|
243
|
+
typeof rawEntityType === "string" &&
|
|
244
|
+
!(ENGRAM_COACH_ENTITY_TYPES as readonly string[]).includes(rawEntityType)
|
|
245
|
+
) {
|
|
246
|
+
errors.push(`candidates[${index}].entity_type "${rawEntityType}" is not a known entity type`);
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
const rawKeyed = entry.keyed_entity_type;
|
|
250
|
+
if (
|
|
251
|
+
rawKeyed !== undefined && rawKeyed !== null &&
|
|
252
|
+
!(KEYED_ENTITY_TYPES as readonly string[]).includes(String(rawKeyed))
|
|
253
|
+
) {
|
|
254
|
+
errors.push(
|
|
255
|
+
`candidates[${index}].keyed_entity_type must be one of ${KEYED_ENTITY_TYPES.join(", ")} or null`,
|
|
256
|
+
);
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
const rawEffectiveAt = entry.effective_at;
|
|
260
|
+
if (
|
|
261
|
+
rawEffectiveAt !== undefined && rawEffectiveAt !== null &&
|
|
262
|
+
!(typeof rawEffectiveAt === "string" && /^\d{4}-\d{2}-\d{2}$/.test(rawEffectiveAt))
|
|
263
|
+
) {
|
|
264
|
+
errors.push(`candidates[${index}].effective_at must be YYYY-MM-DD or null`);
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
candidates.push({
|
|
269
|
+
kind: kind as KnowledgeKind,
|
|
270
|
+
statement: normalizeStatement(rawStatement),
|
|
271
|
+
entityType: typeof rawEntityType === "string" ? rawEntityType : null,
|
|
272
|
+
keyedEntityType: typeof rawKeyed === "string" ? rawKeyed as KeyedEntityType : null,
|
|
273
|
+
keyComponents: jsonObject(entry.key_components),
|
|
274
|
+
effectiveAt: typeof rawEffectiveAt === "string" ? rawEffectiveAt : null,
|
|
275
|
+
subjects: stringArray(entry.subjects),
|
|
276
|
+
topics: stringArray(entry.topics),
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (errors.length > 0) return { ok: false, errors };
|
|
281
|
+
// The cap is pack policy, not a model promise: trim rather than reject.
|
|
282
|
+
return { ok: true, candidates: candidates.slice(0, maxCandidates) };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// ---------------------------------------------------------------------------
|
|
286
|
+
// Extraction with exactly one repair attempt
|
|
287
|
+
// ---------------------------------------------------------------------------
|
|
288
|
+
|
|
289
|
+
export type AmbientExtraction = {
|
|
290
|
+
candidates: AmbientCandidate[];
|
|
291
|
+
warnings: string[];
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Run the configured extraction model over the latest user text. A malformed
|
|
296
|
+
* first response earns exactly one repair prompt carrying the concrete
|
|
297
|
+
* validation errors. A second malformed response, a timeout, a cancellation,
|
|
298
|
+
* or a model failure yields zero candidates and one warning.
|
|
299
|
+
*/
|
|
300
|
+
export async function extractAmbientCandidates(
|
|
301
|
+
userText: string,
|
|
302
|
+
config: EngramCoachRuntimeConfig,
|
|
303
|
+
tools: AmbientCompleter,
|
|
304
|
+
): Promise<AmbientExtraction> {
|
|
305
|
+
const maxCandidates = config.capture.maxCandidatesPerTurn;
|
|
306
|
+
const request: CompletionRequest = {
|
|
307
|
+
model: config.capture.model,
|
|
308
|
+
prompt: extractionPrompt(config, userText, maxCandidates),
|
|
309
|
+
system: SYSTEM_PROMPT,
|
|
310
|
+
timeoutSeconds: config.capture.timeoutSeconds,
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
let response: string;
|
|
314
|
+
try {
|
|
315
|
+
response = await tools.complete(request);
|
|
316
|
+
} catch (error) {
|
|
317
|
+
return { candidates: [], warnings: [completionWarning(error)] };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const first = parseAmbientResponse(response, maxCandidates);
|
|
321
|
+
if (first.ok) return { candidates: first.candidates, warnings: [] };
|
|
322
|
+
|
|
323
|
+
let repaired: string;
|
|
324
|
+
try {
|
|
325
|
+
repaired = await tools.complete({
|
|
326
|
+
...request,
|
|
327
|
+
prompt: repairPrompt(response, first.errors),
|
|
328
|
+
});
|
|
329
|
+
} catch (error) {
|
|
330
|
+
return { candidates: [], warnings: [completionWarning(error)] };
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const second = parseAmbientResponse(repaired, maxCandidates);
|
|
334
|
+
if (second.ok) return { candidates: second.candidates, warnings: [] };
|
|
335
|
+
return { candidates: [], warnings: [AMBIENT_INVALID_JSON_WARNING] };
|
|
336
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* engram-coach typed capture domain input.
|
|
3
|
+
*
|
|
4
|
+
* These types cross the OMP tool boundary as JSON, so JSON-facing domain
|
|
5
|
+
* inputs use snake_case. Skills provide domain values and artifact metadata
|
|
6
|
+
* but never record IDs, lifecycle statuses, relationship arrays, or
|
|
7
|
+
* retirement targets — those are pack-owned.
|
|
8
|
+
*
|
|
9
|
+
* Host mechanics DTOs are imported as types from
|
|
10
|
+
* `@isparling/engram-harness/capture-types` and never duplicated here.
|
|
11
|
+
*
|
|
12
|
+
* @module engram-coach-capture-types
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type {
|
|
16
|
+
ArtifactReplacementResult,
|
|
17
|
+
CaptureMutationView,
|
|
18
|
+
CompletionRequest,
|
|
19
|
+
HostCaptureApply,
|
|
20
|
+
HostCapturePreview,
|
|
21
|
+
} from "@isparling/engram-harness/capture-types";
|
|
22
|
+
import type { JsonObject, KnowledgeEnvelope, KnowledgeError } from "@isparling/engram-harness/knowledge-types";
|
|
23
|
+
import type { EngramCoachSkill } from "./engram-coach-domain.ts";
|
|
24
|
+
|
|
25
|
+
/** Current schema version for every structured capture payload. */
|
|
26
|
+
export const SCHEMA_VERSION = 0;
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Explicit skill capture — typed domain input
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
export type RecordRole = "state" | "event" | "report-claim";
|
|
33
|
+
|
|
34
|
+
export type StructuredCaptureSource = {
|
|
35
|
+
skill: EngramCoachSkill;
|
|
36
|
+
session_id: string;
|
|
37
|
+
turn_id: number;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type StructuredStateChange = {
|
|
41
|
+
entity_type: "workout" | "prescription" | "threshold" | "persona" | "monitoring";
|
|
42
|
+
key_components: JsonObject;
|
|
43
|
+
effective_at: string;
|
|
44
|
+
statement: string;
|
|
45
|
+
details: JsonObject;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export type StructuredEvent = {
|
|
49
|
+
entity_type: "consultation" | "workout-adaptation" | "monitoring-event";
|
|
50
|
+
effective_at: string;
|
|
51
|
+
statement: string;
|
|
52
|
+
action_targets: string[];
|
|
53
|
+
details: JsonObject;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export type StructuredReportClaim = {
|
|
57
|
+
entity_type:
|
|
58
|
+
| "race-conclusion"
|
|
59
|
+
| "block-conclusion"
|
|
60
|
+
| "season-conclusion"
|
|
61
|
+
| "methodology-conclusion"
|
|
62
|
+
| "arc-conclusion";
|
|
63
|
+
key_components: JsonObject;
|
|
64
|
+
effective_at: string;
|
|
65
|
+
statement: string;
|
|
66
|
+
source_document: string;
|
|
67
|
+
details: JsonObject;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export type StructuredChangeSet = {
|
|
71
|
+
schema_version: 0;
|
|
72
|
+
source: StructuredCaptureSource;
|
|
73
|
+
state_changes: StructuredStateChange[];
|
|
74
|
+
events: StructuredEvent[];
|
|
75
|
+
report_claims: StructuredReportClaim[];
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/** Builds an empty, well-formed change set for a skill session/turn. */
|
|
79
|
+
export function makeEmptyChangeSet(source: StructuredCaptureSource): StructuredChangeSet {
|
|
80
|
+
return {
|
|
81
|
+
schema_version: SCHEMA_VERSION,
|
|
82
|
+
source,
|
|
83
|
+
state_changes: [],
|
|
84
|
+
events: [],
|
|
85
|
+
report_claims: [],
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
// Preview — JSON-safe ready/blocked union
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Pack-facing capture preview, mirroring the host DTO union: a ready preview
|
|
95
|
+
* carries the exact plan hash the athlete approves plus the record mutations
|
|
96
|
+
* it would commit; a blocked preview carries validation errors and never
|
|
97
|
+
* reaches Phase 4 presentation.
|
|
98
|
+
*/
|
|
99
|
+
export type CapturePreview = ReadyCapturePreview | BlockedCapturePreview;
|
|
100
|
+
|
|
101
|
+
/** One per-entity row of the ready preview, in plan order. */
|
|
102
|
+
export type CaptureChangeView = {
|
|
103
|
+
entityKey: string | null;
|
|
104
|
+
recordRole: RecordRole;
|
|
105
|
+
classification: "new" | "no-change" | "refine" | "supersede" | "append" | "support";
|
|
106
|
+
creates: string[];
|
|
107
|
+
retires: string[];
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/** Ready preview shape emitted by `previewStructuredCapture`. */
|
|
111
|
+
export type ReadyCapturePreview = {
|
|
112
|
+
schemaVersion: 0;
|
|
113
|
+
status: "ready";
|
|
114
|
+
planHash: string;
|
|
115
|
+
/**
|
|
116
|
+
* The private aggregate candidate. Retained by the extension to bind the
|
|
117
|
+
* approved hash to the exact submitted envelope; NEVER rendered to the
|
|
118
|
+
* model — extension output shows only `planHash`, `changes`, `artifacts`.
|
|
119
|
+
*/
|
|
120
|
+
candidate: KnowledgeEnvelope;
|
|
121
|
+
changes: CaptureChangeView[];
|
|
122
|
+
artifacts: string[];
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
/** Blocked preview shape: pack or host errors, verbatim, no proposal. */
|
|
126
|
+
export type BlockedCapturePreview = {
|
|
127
|
+
schemaVersion: 0;
|
|
128
|
+
status: "blocked";
|
|
129
|
+
errors: KnowledgeError[];
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
// Apply — the committed plan handed to materialization
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* The applied-plan input to materialization: the host's apply result after a
|
|
138
|
+
* successful commit, carrying the approved plan hash and the exact record
|
|
139
|
+
* mutations that were committed. Materialization retry is idempotent and
|
|
140
|
+
* never reapplies these record mutations.
|
|
141
|
+
*/
|
|
142
|
+
export type AppliedCapturePlan = Pick<HostCaptureApply, "planHash" | "mutations">;
|
|
143
|
+
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
// Ambient capture summary
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Result of ambient conversation capture. `warnings` carries visible,
|
|
150
|
+
* non-blocking diagnostics — LLM timeout, cancellation, model failure, or
|
|
151
|
+
* unrepaired JSON produce no draft and only a warning.
|
|
152
|
+
*/
|
|
153
|
+
export type CaptureSummary = {
|
|
154
|
+
created: string[];
|
|
155
|
+
existing: string[];
|
|
156
|
+
invalid: Array<{ id: string; errors: string[] }>;
|
|
157
|
+
warnings: string[];
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
// Materialization result
|
|
162
|
+
// ---------------------------------------------------------------------------
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Outcome of regenerating compatibility views after a committed apply.
|
|
166
|
+
* `written` and `unchanged` mirror the host's artifact-replacement result;
|
|
167
|
+
* `stale` lists views whose regeneration failed and must be retried
|
|
168
|
+
* idempotently — record commit remains authoritative regardless.
|
|
169
|
+
*/
|
|
170
|
+
export type MaterializationResult = {
|
|
171
|
+
written: ArtifactReplacementResult[];
|
|
172
|
+
unchanged: ArtifactReplacementResult[];
|
|
173
|
+
stale: Array<{ path: string; reason: string }>;
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
// Re-exported host mechanics DTOs for sibling capture modules (ambient
|
|
177
|
+
// completion uses CompletionRequest; reconciliation consumes
|
|
178
|
+
// CaptureMutationView). Types only — no runtime surface.
|
|
179
|
+
export type {
|
|
180
|
+
ArtifactReplacementResult,
|
|
181
|
+
CaptureMutationView,
|
|
182
|
+
CompletionRequest,
|
|
183
|
+
HostCapturePreview,
|
|
184
|
+
KnowledgeError,
|
|
185
|
+
};
|