@cleocode/lafs-protocol 1.4.0 → 1.5.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.
@@ -86,6 +86,15 @@ export class TaskNotFoundError extends Error {
86
86
  this.taskId = taskId;
87
87
  }
88
88
  }
89
+ /** Thrown when a refinement/follow-up task references invalid parent tasks */
90
+ export class TaskRefinementError extends Error {
91
+ referenceTaskIds;
92
+ constructor(message, referenceTaskIds) {
93
+ super(message);
94
+ this.name = 'TaskRefinementError';
95
+ this.referenceTaskIds = referenceTaskIds;
96
+ }
97
+ }
89
98
  // ============================================================================
90
99
  // ID Generation
91
100
  // ============================================================================
@@ -106,7 +115,15 @@ export class TaskManager {
106
115
  /** Create a new task in the submitted state */
107
116
  createTask(options) {
108
117
  const id = generateId();
109
- const contextId = options?.contextId ?? generateId();
118
+ const resolvedContextId = options?.contextId ?? this.resolveContextForReferenceTasks(options?.referenceTaskIds) ?? generateId();
119
+ const contextId = resolvedContextId;
120
+ const referenceTaskIds = options?.referenceTaskIds ?? [];
121
+ this.validateReferenceTasks(referenceTaskIds, contextId);
122
+ const metadata = {
123
+ ...(options?.metadata ?? {}),
124
+ ...(referenceTaskIds.length > 0 ? { referenceTaskIds } : {}),
125
+ ...(options?.parallelFollowUp ? { parallelFollowUp: true } : {}),
126
+ };
110
127
  const task = {
111
128
  id,
112
129
  contextId,
@@ -115,7 +132,7 @@ export class TaskManager {
115
132
  state: 'submitted',
116
133
  timestamp: new Date().toISOString(),
117
134
  },
118
- ...(options?.metadata && { metadata: options.metadata }),
135
+ ...(Object.keys(metadata).length > 0 && { metadata }),
119
136
  };
120
137
  this.tasks.set(id, task);
121
138
  // Index by contextId
@@ -127,6 +144,14 @@ export class TaskManager {
127
144
  contextTasks.add(id);
128
145
  return structuredClone(task);
129
146
  }
147
+ /** Create a refinement/follow-up task referencing existing task(s). */
148
+ createRefinedTask(referenceTaskIds, options) {
149
+ return this.createTask({
150
+ ...options,
151
+ referenceTaskIds,
152
+ parallelFollowUp: options?.parallelFollowUp,
153
+ });
154
+ }
130
155
  /** Get a task by ID. Throws TaskNotFoundError if not found. */
131
156
  getTask(taskId) {
132
157
  const task = this.tasks.get(taskId);
@@ -249,6 +274,31 @@ export class TaskManager {
249
274
  }
250
275
  return isTerminalState(task.status.state);
251
276
  }
277
+ resolveContextForReferenceTasks(referenceTaskIds) {
278
+ if (!referenceTaskIds || referenceTaskIds.length === 0) {
279
+ return undefined;
280
+ }
281
+ const firstId = referenceTaskIds[0];
282
+ if (!firstId) {
283
+ return undefined;
284
+ }
285
+ const first = this.tasks.get(firstId);
286
+ return first?.contextId;
287
+ }
288
+ validateReferenceTasks(referenceTaskIds, contextId) {
289
+ if (referenceTaskIds.length === 0) {
290
+ return;
291
+ }
292
+ for (const refId of referenceTaskIds) {
293
+ const refTask = this.tasks.get(refId);
294
+ if (!refTask) {
295
+ throw new TaskRefinementError(`Referenced task not found: ${refId}`, referenceTaskIds);
296
+ }
297
+ if (refTask.contextId !== contextId) {
298
+ throw new TaskRefinementError(`Referenced task ${refId} has different contextId (${refTask.contextId}) than refinement (${contextId})`, referenceTaskIds);
299
+ }
300
+ }
301
+ }
252
302
  }
253
303
  // ============================================================================
254
304
  // LAFS Integration
@@ -1,3 +1,7 @@
1
1
  import type { ConformanceReport, FlagInput } from "./types.js";
2
- export declare function runEnvelopeConformance(envelope: unknown): ConformanceReport;
2
+ import { type ConformanceTier } from "./conformanceProfiles.js";
3
+ export interface EnvelopeConformanceOptions {
4
+ tier?: ConformanceTier;
5
+ }
6
+ export declare function runEnvelopeConformance(envelope: unknown, options?: EnvelopeConformanceOptions): ConformanceReport;
3
7
  export declare function runFlagConformance(flags: FlagInput): ConformanceReport;
@@ -1,10 +1,12 @@
1
- import { isRegisteredErrorCode } from "./errorRegistry.js";
1
+ import { getTransportMapping, isRegisteredErrorCode } from "./errorRegistry.js";
2
2
  import { resolveOutputFormat, LAFSFlagError } from "./flagSemantics.js";
3
+ import { isMVILevel } from "./types.js";
4
+ import { getChecksForTier } from "./conformanceProfiles.js";
3
5
  import { validateEnvelope } from "./validateEnvelope.js";
4
6
  function pushCheck(checks, name, pass, detail) {
5
7
  checks.push({ name, pass, ...(detail ? { detail } : {}) });
6
8
  }
7
- export function runEnvelopeConformance(envelope) {
9
+ export function runEnvelopeConformance(envelope, options = {}) {
8
10
  const checks = [];
9
11
  const validation = validateEnvelope(envelope);
10
12
  pushCheck(checks, "envelope_schema_valid", validation.valid, validation.valid ? undefined : validation.errors.join("; "));
@@ -30,8 +32,52 @@ export function runEnvelopeConformance(envelope) {
30
32
  else {
31
33
  pushCheck(checks, "error_code_registered", true, "error field absent or null — skipped (optional when success=true)");
32
34
  }
33
- const validMviLevels = ["minimal", "standard", "full", "custom"];
34
- pushCheck(checks, "meta_mvi_present", validMviLevels.includes(typed._meta.mvi), validMviLevels.includes(typed._meta.mvi) ? undefined : `invalid mvi level: ${String(typed._meta.mvi)}`);
35
+ // transport_mapping_consistent: when an error is present, ensure the code has
36
+ // a transport-specific mapping in the registry for the declared transport.
37
+ if (typed.error) {
38
+ if (typed._meta.transport === "sdk") {
39
+ pushCheck(checks, "transport_mapping_consistent", true, "sdk transport does not require external status-code mapping");
40
+ }
41
+ else {
42
+ const mapping = getTransportMapping(typed.error.code, typed._meta.transport);
43
+ const mappingOk = mapping !== null;
44
+ pushCheck(checks, "transport_mapping_consistent", mappingOk, mappingOk
45
+ ? undefined
46
+ : `no ${typed._meta.transport} mapping found for code ${typed.error.code}`);
47
+ }
48
+ }
49
+ else {
50
+ pushCheck(checks, "transport_mapping_consistent", true, "no error present — mapping check skipped");
51
+ }
52
+ // context_mutation_failure: if the producer marks context as required for a
53
+ // mutation operation, missing context must fail with a context error code.
54
+ {
55
+ const ext = (typed._extensions ?? {});
56
+ const contextObj = (ext["context"] ?? {});
57
+ const lafsObj = (ext["lafs"] ?? {});
58
+ const contextRequired = ext["lafsContextRequired"] === true ||
59
+ contextObj["required"] === true ||
60
+ lafsObj["contextRequired"] === true;
61
+ if (!contextRequired) {
62
+ pushCheck(checks, "context_mutation_failure", true, "context not marked required — skipped");
63
+ }
64
+ else {
65
+ const hasContextIdentity = typed._meta.contextVersion > 0 || Boolean(typed._meta.sessionId);
66
+ if (typed.success) {
67
+ const pass = hasContextIdentity;
68
+ pushCheck(checks, "context_mutation_failure", pass, pass ? undefined : "context required but missing identity (expect E_CONTEXT_MISSING)");
69
+ }
70
+ else {
71
+ const code = typed.error?.code;
72
+ const pass = code === "E_CONTEXT_MISSING" || code === "E_CONTEXT_STALE";
73
+ pushCheck(checks, "context_mutation_failure", pass, pass
74
+ ? undefined
75
+ : `context required failures should return E_CONTEXT_MISSING or E_CONTEXT_STALE, got ${String(code)}`);
76
+ }
77
+ }
78
+ }
79
+ const mviValid = isMVILevel(typed._meta.mvi);
80
+ pushCheck(checks, "meta_mvi_present", mviValid, mviValid ? undefined : `invalid mvi level: ${String(typed._meta.mvi)}`);
35
81
  pushCheck(checks, "meta_strict_present", typeof typed._meta.strict === "boolean");
36
82
  // strict_mode_behavior: when strict=true, the envelope MUST NOT contain
37
83
  // explicit null for optional fields that can be omitted (page, error on success).
@@ -72,6 +118,9 @@ export function runEnvelopeConformance(envelope) {
72
118
  }
73
119
  pushCheck(checks, "pagination_mode_consistent", consistent, consistent ? undefined : detail);
74
120
  }
121
+ else {
122
+ pushCheck(checks, "pagination_mode_consistent", true, "page absent — skipped");
123
+ }
75
124
  // strict_mode_enforced: verify the schema enforces additional-property rules.
76
125
  // When strict=true, extra top-level properties must be rejected by validation.
77
126
  // When strict=false, extra top-level properties must be allowed.
@@ -85,7 +134,59 @@ export function runEnvelopeConformance(envelope) {
85
134
  pushCheck(checks, "strict_mode_enforced", extraResult.valid, !extraResult.valid ? "strict=false but additional properties were rejected" : undefined);
86
135
  }
87
136
  }
88
- return { ok: checks.every((check) => check.pass), checks };
137
+ // context_preservation_valid: validate monotonic context version behavior and
138
+ // context-constraint integrity when a context ledger extension is present.
139
+ {
140
+ const ext = (typed._extensions ?? {});
141
+ const ledger = (ext["contextLedger"] ?? ext["context"]);
142
+ if (!ledger || typeof ledger !== "object") {
143
+ pushCheck(checks, "context_preservation_valid", true, "context ledger absent — skipped");
144
+ }
145
+ else {
146
+ const version = ledger["version"];
147
+ const previousVersion = ledger["previousVersion"];
148
+ const removedConstraints = ledger["removedConstraints"];
149
+ const hasNumericVersion = typeof version === "number";
150
+ const matchesEnvelopeVersion = hasNumericVersion && version === typed._meta.contextVersion;
151
+ const monotonicFromPrevious = typeof previousVersion !== "number" || (hasNumericVersion && version >= previousVersion);
152
+ const constraintsPreserved = !Array.isArray(removedConstraints) || removedConstraints.length === 0 || !typed.success;
153
+ let pass = matchesEnvelopeVersion && monotonicFromPrevious && constraintsPreserved;
154
+ let detail;
155
+ if (!hasNumericVersion) {
156
+ pass = false;
157
+ detail = "context ledger version must be numeric";
158
+ }
159
+ else if (!matchesEnvelopeVersion) {
160
+ detail = `context version mismatch: ledger=${String(version)} envelope=${typed._meta.contextVersion}`;
161
+ }
162
+ else if (!monotonicFromPrevious) {
163
+ detail = `non-monotonic context version: previous=${String(previousVersion)} current=${String(version)}`;
164
+ }
165
+ else if (!constraintsPreserved) {
166
+ detail = "context constraint removal detected on successful response";
167
+ }
168
+ // Error-path validation for stale/missing context signaling.
169
+ if (!typed.success && typed.error && ledger["required"] === true) {
170
+ const stale = ledger["stale"] === true;
171
+ if (stale && typed.error.code !== "E_CONTEXT_STALE") {
172
+ pass = false;
173
+ detail = `stale context should return E_CONTEXT_STALE, got ${typed.error.code}`;
174
+ }
175
+ if (!stale && typed.error.code !== "E_CONTEXT_MISSING" && typed.error.code !== "E_CONTEXT_STALE") {
176
+ pass = false;
177
+ detail = `required context failure should return E_CONTEXT_MISSING or E_CONTEXT_STALE, got ${typed.error.code}`;
178
+ }
179
+ }
180
+ pushCheck(checks, "context_preservation_valid", pass, detail);
181
+ }
182
+ }
183
+ const tier = options.tier;
184
+ if (!tier) {
185
+ return { ok: checks.every((check) => check.pass), checks };
186
+ }
187
+ const allowed = new Set(getChecksForTier(tier));
188
+ const tierChecks = checks.filter((check) => allowed.has(check.name));
189
+ return { ok: tierChecks.every((check) => check.pass), checks: tierChecks };
89
190
  }
90
191
  export function runFlagConformance(flags) {
91
192
  const checks = [];
@@ -0,0 +1,11 @@
1
+ export type ConformanceTier = "core" | "standard" | "complete";
2
+ export interface ConformanceProfiles {
3
+ version: string;
4
+ tiers: Record<ConformanceTier, string[]>;
5
+ }
6
+ export declare function getConformanceProfiles(): ConformanceProfiles;
7
+ export declare function getChecksForTier(tier: ConformanceTier): string[];
8
+ export declare function validateConformanceProfiles(availableChecks: string[]): {
9
+ valid: boolean;
10
+ errors: string[];
11
+ };
@@ -0,0 +1,34 @@
1
+ import profilesJson from "../schemas/v1/conformance-profiles.json" with { type: "json" };
2
+ export function getConformanceProfiles() {
3
+ return profilesJson;
4
+ }
5
+ export function getChecksForTier(tier) {
6
+ const profiles = getConformanceProfiles();
7
+ return profiles.tiers[tier] ?? [];
8
+ }
9
+ export function validateConformanceProfiles(availableChecks) {
10
+ const profiles = getConformanceProfiles();
11
+ const errors = [];
12
+ const core = new Set(profiles.tiers.core);
13
+ const standard = new Set(profiles.tiers.standard);
14
+ const complete = new Set(profiles.tiers.complete);
15
+ for (const check of core) {
16
+ if (!standard.has(check)) {
17
+ errors.push(`standard tier must include core check: ${check}`);
18
+ }
19
+ }
20
+ for (const check of standard) {
21
+ if (!complete.has(check)) {
22
+ errors.push(`complete tier must include standard check: ${check}`);
23
+ }
24
+ }
25
+ const known = new Set(availableChecks);
26
+ for (const [tier, checks] of Object.entries(profiles.tiers)) {
27
+ for (const check of checks) {
28
+ if (!known.has(check)) {
29
+ errors.push(`unknown check in ${tier} tier: ${check}`);
30
+ }
31
+ }
32
+ }
33
+ return { valid: errors.length === 0, errors };
34
+ }
@@ -0,0 +1,13 @@
1
+ import type { LAFSEnvelope, Warning } from "./types.js";
2
+ export interface DeprecationEntry {
3
+ id: string;
4
+ code: string;
5
+ message: string;
6
+ deprecated: string;
7
+ replacement?: string;
8
+ removeBy: string;
9
+ detector: (envelope: LAFSEnvelope) => boolean;
10
+ }
11
+ export declare function getDeprecationRegistry(): DeprecationEntry[];
12
+ export declare function detectDeprecatedEnvelopeFields(envelope: LAFSEnvelope): Warning[];
13
+ export declare function emitDeprecationWarnings(envelope: LAFSEnvelope): LAFSEnvelope;
@@ -0,0 +1,39 @@
1
+ const DEPRECATION_REGISTRY = [
2
+ {
3
+ id: "meta-mvi-boolean",
4
+ code: "W_DEPRECATED_META_MVI_BOOLEAN",
5
+ message: "_meta.mvi boolean values are deprecated",
6
+ deprecated: "1.0.0",
7
+ replacement: "Use _meta.mvi as one of: minimal|standard|full|custom",
8
+ removeBy: "2.0.0",
9
+ detector: (envelope) => typeof envelope._meta.mvi === "boolean",
10
+ },
11
+ ];
12
+ export function getDeprecationRegistry() {
13
+ return DEPRECATION_REGISTRY;
14
+ }
15
+ export function detectDeprecatedEnvelopeFields(envelope) {
16
+ return getDeprecationRegistry()
17
+ .filter((entry) => entry.detector(envelope))
18
+ .map((entry) => ({
19
+ code: entry.code,
20
+ message: entry.message,
21
+ deprecated: entry.deprecated,
22
+ replacement: entry.replacement,
23
+ removeBy: entry.removeBy,
24
+ }));
25
+ }
26
+ export function emitDeprecationWarnings(envelope) {
27
+ const detected = detectDeprecatedEnvelopeFields(envelope);
28
+ if (detected.length === 0) {
29
+ return envelope;
30
+ }
31
+ const existingWarnings = envelope._meta.warnings ?? [];
32
+ return {
33
+ ...envelope,
34
+ _meta: {
35
+ ...envelope._meta,
36
+ warnings: [...existingWarnings, ...detected],
37
+ },
38
+ };
39
+ }
@@ -11,5 +11,11 @@ export interface ErrorRegistry {
11
11
  version: string;
12
12
  codes: RegistryCode[];
13
13
  }
14
+ export type TransportMapping = {
15
+ transport: "http" | "grpc" | "cli";
16
+ value: number | string;
17
+ };
14
18
  export declare function getErrorRegistry(): ErrorRegistry;
15
19
  export declare function isRegisteredErrorCode(code: string): boolean;
20
+ export declare function getRegistryCode(code: string): RegistryCode | undefined;
21
+ export declare function getTransportMapping(code: string, transport: "http" | "grpc" | "cli"): TransportMapping | null;
@@ -6,3 +6,19 @@ export function isRegisteredErrorCode(code) {
6
6
  const registry = getErrorRegistry();
7
7
  return registry.codes.some((item) => item.code === code);
8
8
  }
9
+ export function getRegistryCode(code) {
10
+ return getErrorRegistry().codes.find((item) => item.code === code);
11
+ }
12
+ export function getTransportMapping(code, transport) {
13
+ const registryCode = getRegistryCode(code);
14
+ if (!registryCode) {
15
+ return null;
16
+ }
17
+ if (transport === "http") {
18
+ return { transport, value: registryCode.httpStatus };
19
+ }
20
+ if (transport === "grpc") {
21
+ return { transport, value: registryCode.grpcStatus };
22
+ }
23
+ return { transport, value: registryCode.cliExit };
24
+ }
@@ -0,0 +1,67 @@
1
+ import type { LAFSEnvelope, MVILevel } from "./types.js";
2
+ export interface FieldExtractionInput {
3
+ /** --field <name>: extract single field as plain text, no envelope */
4
+ fieldFlag?: string;
5
+ /** --fields <a,b,c>: filter result to these fields, preserve envelope */
6
+ fieldsFlag?: string | string[];
7
+ /** --mvi <level>: envelope verbosity (client-requestable levels only) */
8
+ mviFlag?: MVILevel | string;
9
+ }
10
+ export interface FieldExtractionResolution {
11
+ /** When set: extract this field as plain text, discard envelope. */
12
+ field?: string;
13
+ /** When set: filter result to these fields (envelope preserved). */
14
+ fields?: string[];
15
+ /** Resolved MVI level. Defaults to 'standard'. */
16
+ mvi: MVILevel;
17
+ /** Which input determined the mvi value: 'flag' when mviFlag was valid, 'default' otherwise. */
18
+ mviSource: "flag" | "default";
19
+ /**
20
+ * True when _fields are requested, indicating the server SHOULD set
21
+ * _meta.mvi = 'custom' in the response per §9.1.
22
+ * Separate from the client-resolved mvi level.
23
+ */
24
+ expectsCustomMvi: boolean;
25
+ }
26
+ export declare function resolveFieldExtraction(input: FieldExtractionInput): FieldExtractionResolution;
27
+ /**
28
+ * Extract a named field from a LAFS result object.
29
+ *
30
+ * Handles four result shapes:
31
+ * 1. Direct array: result[0][field] (list operations where result IS an array)
32
+ * 2. Direct: result[field] (flat result object)
33
+ * 3. Nested: result.<key>[field] (wrapper-entity, e.g. result.task.title)
34
+ * 4. Array value: result.<key>[0][field] (wrapper-array, e.g. result.items[0].title)
35
+ *
36
+ * Returns the value from the first match only. For array results (shapes 1
37
+ * and 4), returns the first element's field value only. To extract from all
38
+ * elements, iterate the array or use applyFieldFilter().
39
+ *
40
+ * When multiple wrapper keys contain the requested field (shapes 3 and 4),
41
+ * the first key in property insertion order wins.
42
+ *
43
+ * Returns undefined if not found at any level.
44
+ */
45
+ export declare function extractFieldFromResult(result: LAFSEnvelope['result'], field: string): unknown;
46
+ /** Convenience wrapper — extracts a field from an envelope's result. */
47
+ export declare function extractFieldFromEnvelope(envelope: LAFSEnvelope, field: string): unknown;
48
+ /**
49
+ * Filter result fields in a LAFS envelope to the requested subset.
50
+ *
51
+ * Handles the same four result shapes as extractFieldFromResult:
52
+ * 1. Direct array: project each element
53
+ * 2. Flat result: project top-level keys
54
+ * 3. Wrapper-entity: project nested entity's keys, preserve wrapper
55
+ * 4. Wrapper-array: project each element's keys, preserve wrapper
56
+ *
57
+ * Sets _meta.mvi = 'custom' per §9.1.
58
+ * Returns a new envelope with a new _meta object. Result values are not
59
+ * deep-cloned; nested object references are shared with the original.
60
+ * Unknown field names are silently omitted per §9.2.
61
+ *
62
+ * When result is a wrapper (shapes 3/4) with multiple keys, each key is
63
+ * projected independently. Primitive values at the wrapper level (numbers,
64
+ * strings, booleans) are preserved as-is — _fields is applied to nested
65
+ * entity or array keys only, not to the wrapper's own primitive keys.
66
+ */
67
+ export declare function applyFieldFilter(envelope: LAFSEnvelope, fields: string[]): LAFSEnvelope;
@@ -0,0 +1,133 @@
1
+ import { LAFSFlagError } from "./flagSemantics.js";
2
+ import { isMVILevel } from "./types.js";
3
+ export function resolveFieldExtraction(input) {
4
+ if (input.fieldFlag && input.fieldsFlag) {
5
+ throw new LAFSFlagError('E_FIELD_CONFLICT', 'Cannot combine --field and --fields: --field extracts a single value '
6
+ + 'as plain text (no envelope); --fields filters the JSON envelope. '
7
+ + 'Use one or the other.', { conflictingModes: ['single-field-extraction', 'multi-field-filter'] });
8
+ }
9
+ const fields = typeof input.fieldsFlag === 'string'
10
+ ? input.fieldsFlag.split(',').map(f => f.trim()).filter(Boolean)
11
+ : Array.isArray(input.fieldsFlag)
12
+ ? input.fieldsFlag.map(f => f.trim()).filter(Boolean)
13
+ : undefined;
14
+ // 'custom' is server-set (§9.1) — not a client-requestable level
15
+ const validMvi = isMVILevel(input.mviFlag) && input.mviFlag !== 'custom';
16
+ const mvi = validMvi ? input.mviFlag : 'standard';
17
+ const mviSource = validMvi ? 'flag' : 'default';
18
+ const hasFields = (fields?.length ?? 0) > 0;
19
+ return {
20
+ field: input.fieldFlag || undefined,
21
+ fields: hasFields ? fields : undefined,
22
+ mvi,
23
+ mviSource,
24
+ expectsCustomMvi: hasFields,
25
+ };
26
+ }
27
+ /**
28
+ * Extract a named field from a LAFS result object.
29
+ *
30
+ * Handles four result shapes:
31
+ * 1. Direct array: result[0][field] (list operations where result IS an array)
32
+ * 2. Direct: result[field] (flat result object)
33
+ * 3. Nested: result.<key>[field] (wrapper-entity, e.g. result.task.title)
34
+ * 4. Array value: result.<key>[0][field] (wrapper-array, e.g. result.items[0].title)
35
+ *
36
+ * Returns the value from the first match only. For array results (shapes 1
37
+ * and 4), returns the first element's field value only. To extract from all
38
+ * elements, iterate the array or use applyFieldFilter().
39
+ *
40
+ * When multiple wrapper keys contain the requested field (shapes 3 and 4),
41
+ * the first key in property insertion order wins.
42
+ *
43
+ * Returns undefined if not found at any level.
44
+ */
45
+ export function extractFieldFromResult(result, field) {
46
+ if (result === null || typeof result !== 'object')
47
+ return undefined;
48
+ // Shape 1: result is a direct array
49
+ if (Array.isArray(result)) {
50
+ if (result.length === 0)
51
+ return undefined;
52
+ const first = result[0];
53
+ if (first && typeof first === 'object' && field in first)
54
+ return first[field];
55
+ return undefined;
56
+ }
57
+ // Shape 2: direct property on result object
58
+ const record = result;
59
+ if (field in record)
60
+ return record[field];
61
+ // Shapes 3 & 4: one level down (first matching key in insertion order wins)
62
+ for (const value of Object.values(record)) {
63
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
64
+ const nested = value;
65
+ if (field in nested)
66
+ return nested[field];
67
+ }
68
+ if (Array.isArray(value) && value.length > 0) {
69
+ const first = value[0];
70
+ if (first && typeof first === 'object' && field in first)
71
+ return first[field];
72
+ }
73
+ }
74
+ return undefined;
75
+ }
76
+ /** Convenience wrapper — extracts a field from an envelope's result. */
77
+ export function extractFieldFromEnvelope(envelope, field) {
78
+ return extractFieldFromResult(envelope.result, field);
79
+ }
80
+ /**
81
+ * Filter result fields in a LAFS envelope to the requested subset.
82
+ *
83
+ * Handles the same four result shapes as extractFieldFromResult:
84
+ * 1. Direct array: project each element
85
+ * 2. Flat result: project top-level keys
86
+ * 3. Wrapper-entity: project nested entity's keys, preserve wrapper
87
+ * 4. Wrapper-array: project each element's keys, preserve wrapper
88
+ *
89
+ * Sets _meta.mvi = 'custom' per §9.1.
90
+ * Returns a new envelope with a new _meta object. Result values are not
91
+ * deep-cloned; nested object references are shared with the original.
92
+ * Unknown field names are silently omitted per §9.2.
93
+ *
94
+ * When result is a wrapper (shapes 3/4) with multiple keys, each key is
95
+ * projected independently. Primitive values at the wrapper level (numbers,
96
+ * strings, booleans) are preserved as-is — _fields is applied to nested
97
+ * entity or array keys only, not to the wrapper's own primitive keys.
98
+ */
99
+ export function applyFieldFilter(envelope, fields) {
100
+ if (fields.length === 0 || envelope.result === null)
101
+ return envelope;
102
+ const pick = (obj) => Object.fromEntries(fields.filter(f => f in obj).map(f => [f, obj[f]]));
103
+ let filtered;
104
+ if (Array.isArray(envelope.result)) {
105
+ // Shape 1: direct array
106
+ filtered = envelope.result.map(pick);
107
+ }
108
+ else {
109
+ const record = envelope.result;
110
+ const topLevelMatch = fields.some(f => f in record);
111
+ if (topLevelMatch) {
112
+ // Shape 2: flat result
113
+ filtered = pick(record);
114
+ }
115
+ else {
116
+ // Shapes 3 & 4: wrapper — apply pick one level down, preserve wrapper keys
117
+ filtered = Object.fromEntries(Object.entries(record).map(([k, v]) => {
118
+ if (Array.isArray(v)) {
119
+ return [k, v.map(item => pick(item))];
120
+ }
121
+ if (v && typeof v === 'object') {
122
+ return [k, pick(v)];
123
+ }
124
+ return [k, v];
125
+ }));
126
+ }
127
+ }
128
+ return {
129
+ ...envelope,
130
+ _meta: { ...envelope._meta, mvi: 'custom' },
131
+ result: filtered,
132
+ };
133
+ }
@@ -1,12 +1,16 @@
1
- import type { FlagInput } from "./types.js";
1
+ import type { FlagInput, LAFSError, LAFSErrorCategory } from "./types.js";
2
2
  export interface FlagResolution {
3
3
  format: "json" | "human";
4
4
  source: "flag" | "project" | "user" | "default";
5
5
  /** When true, suppress non-essential output for scripting */
6
6
  quiet: boolean;
7
7
  }
8
- export declare class LAFSFlagError extends Error {
8
+ export declare class LAFSFlagError extends Error implements LAFSError {
9
9
  code: string;
10
- constructor(code: string, message: string);
10
+ category: LAFSErrorCategory;
11
+ retryable: boolean;
12
+ retryAfterMs: number | null;
13
+ details: Record<string, unknown>;
14
+ constructor(code: string, message: string, details?: Record<string, unknown>);
11
15
  }
12
16
  export declare function resolveOutputFormat(input: FlagInput): FlagResolution;
@@ -1,9 +1,19 @@
1
+ import { getRegistryCode } from "./errorRegistry.js";
1
2
  export class LAFSFlagError extends Error {
2
3
  code;
3
- constructor(code, message) {
4
+ category;
5
+ retryable;
6
+ retryAfterMs;
7
+ details;
8
+ constructor(code, message, details = {}) {
4
9
  super(message);
5
10
  this.name = "LAFSFlagError";
6
11
  this.code = code;
12
+ const entry = getRegistryCode(code);
13
+ this.category = (entry?.category ?? "CONTRACT");
14
+ this.retryable = entry?.retryable ?? false;
15
+ this.retryAfterMs = null;
16
+ this.details = details;
7
17
  }
8
18
  }
9
19
  export function resolveOutputFormat(input) {
@@ -1,9 +1,12 @@
1
1
  export * from "./types.js";
2
2
  export * from "./errorRegistry.js";
3
+ export * from "./deprecationRegistry.js";
3
4
  export * from "./validateEnvelope.js";
4
5
  export * from "./envelope.js";
5
6
  export * from "./flagSemantics.js";
7
+ export * from "./fieldExtraction.js";
6
8
  export * from "./conformance.js";
9
+ export * from "./conformanceProfiles.js";
7
10
  export * from "./compliance.js";
8
11
  export * from "./tokenEstimator.js";
9
12
  export * from "./budgetEnforcement.js";
@@ -12,6 +15,6 @@ export * from "./discovery.js";
12
15
  export * from "./health/index.js";
13
16
  export * from "./shutdown/index.js";
14
17
  export * from "./circuit-breaker/index.js";
15
- export { LafsA2AResult, createLafsArtifact, createTextArtifact, createFileArtifact, isExtensionRequired, getExtensionParams, AGENT_CARD_PATH, HTTP_EXTENSION_HEADER, LAFS_EXTENSION_URI, A2A_EXTENSIONS_HEADER, parseExtensionsHeader, negotiateExtensions, formatExtensionsHeader, buildLafsExtension, ExtensionSupportRequiredError, extensionNegotiationMiddleware, TERMINAL_STATES, INTERRUPTED_STATES, VALID_TRANSITIONS, isValidTransition, isTerminalState, isInterruptedState, InvalidStateTransitionError, TaskImmutabilityError, TaskNotFoundError, TaskManager, attachLafsEnvelope, } from "./a2a/index.js";
16
- export type { LafsA2AConfig, LafsSendMessageParams, LafsExtensionParams, ExtensionNegotiationResult, BuildLafsExtensionOptions, ExtensionNegotiationMiddlewareOptions, CreateTaskOptions, ListTasksOptions, ListTasksResult, } from "./a2a/index.js";
18
+ export { LafsA2AResult, createLafsArtifact, createTextArtifact, createFileArtifact, isExtensionRequired, getExtensionParams, AGENT_CARD_PATH, HTTP_EXTENSION_HEADER, LAFS_EXTENSION_URI, A2A_EXTENSIONS_HEADER, parseExtensionsHeader, negotiateExtensions, formatExtensionsHeader, buildLafsExtension, ExtensionSupportRequiredError, extensionNegotiationMiddleware, TERMINAL_STATES, INTERRUPTED_STATES, VALID_TRANSITIONS, isValidTransition, isTerminalState, isInterruptedState, InvalidStateTransitionError, TaskImmutabilityError, TaskNotFoundError, TaskRefinementError, TaskManager, attachLafsEnvelope, TaskEventBus, PushNotificationConfigStore, PushNotificationDispatcher, TaskArtifactAssembler, streamTaskEvents, } from "./a2a/index.js";
19
+ export type { LafsA2AConfig, LafsSendMessageParams, LafsExtensionParams, ExtensionNegotiationResult, BuildLafsExtensionOptions, ExtensionNegotiationMiddlewareOptions, CreateTaskOptions, ListTasksOptions, ListTasksResult, TaskStreamEvent, StreamIteratorOptions, PushNotificationDeliveryResult, PushTransport, } from "./a2a/index.js";
17
20
  export type { Task, TaskState, TaskStatus, Artifact, Part, Message, PushNotificationConfig, MessageSendConfiguration, TaskStatusUpdateEvent, TaskArtifactUpdateEvent, SendMessageResponse, SendMessageSuccessResponse, JSONRPCErrorResponse, TextPart, DataPart, FilePart, } from "./a2a/index.js";