@cleocode/lafs 1.8.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/LICENSE +21 -0
- package/README.md +235 -0
- package/dist/schemas/v1/conformance-profiles.json +39 -0
- package/dist/schemas/v1/envelope.schema.json +306 -0
- package/dist/schemas/v1/error-registry.json +162 -0
- package/dist/src/a2a/bindings/grpc.d.ts +67 -0
- package/dist/src/a2a/bindings/grpc.js +148 -0
- package/dist/src/a2a/bindings/http.d.ts +102 -0
- package/dist/src/a2a/bindings/http.js +120 -0
- package/dist/src/a2a/bindings/index.d.ts +35 -0
- package/dist/src/a2a/bindings/index.js +79 -0
- package/dist/src/a2a/bindings/jsonrpc.d.ts +77 -0
- package/dist/src/a2a/bindings/jsonrpc.js +114 -0
- package/dist/src/a2a/bridge.d.ts +175 -0
- package/dist/src/a2a/bridge.js +286 -0
- package/dist/src/a2a/extensions.d.ts +121 -0
- package/dist/src/a2a/extensions.js +205 -0
- package/dist/src/a2a/index.d.ts +40 -0
- package/dist/src/a2a/index.js +76 -0
- package/dist/src/a2a/streaming.d.ts +74 -0
- package/dist/src/a2a/streaming.js +265 -0
- package/dist/src/a2a/task-lifecycle.d.ts +109 -0
- package/dist/src/a2a/task-lifecycle.js +313 -0
- package/dist/src/budgetEnforcement.d.ts +84 -0
- package/dist/src/budgetEnforcement.js +328 -0
- package/dist/src/circuit-breaker/index.d.ts +121 -0
- package/dist/src/circuit-breaker/index.js +249 -0
- package/dist/src/cli.d.ts +16 -0
- package/dist/src/cli.js +63 -0
- package/dist/src/compliance.d.ts +31 -0
- package/dist/src/compliance.js +89 -0
- package/dist/src/conformance.d.ts +7 -0
- package/dist/src/conformance.js +248 -0
- package/dist/src/conformanceProfiles.d.ts +11 -0
- package/dist/src/conformanceProfiles.js +34 -0
- package/dist/src/deprecationRegistry.d.ts +13 -0
- package/dist/src/deprecationRegistry.js +39 -0
- package/dist/src/discovery.d.ts +286 -0
- package/dist/src/discovery.js +350 -0
- package/dist/src/envelope.d.ts +60 -0
- package/dist/src/envelope.js +136 -0
- package/dist/src/errorRegistry.d.ts +28 -0
- package/dist/src/errorRegistry.js +36 -0
- package/dist/src/fieldExtraction.d.ts +67 -0
- package/dist/src/fieldExtraction.js +133 -0
- package/dist/src/flagResolver.d.ts +46 -0
- package/dist/src/flagResolver.js +47 -0
- package/dist/src/flagSemantics.d.ts +16 -0
- package/dist/src/flagSemantics.js +45 -0
- package/dist/src/health/index.d.ts +105 -0
- package/dist/src/health/index.js +220 -0
- package/dist/src/index.d.ts +24 -0
- package/dist/src/index.js +34 -0
- package/dist/src/mcpAdapter.d.ts +28 -0
- package/dist/src/mcpAdapter.js +281 -0
- package/dist/src/mviProjection.d.ts +19 -0
- package/dist/src/mviProjection.js +116 -0
- package/dist/src/problemDetails.d.ts +34 -0
- package/dist/src/problemDetails.js +45 -0
- package/dist/src/shutdown/index.d.ts +69 -0
- package/dist/src/shutdown/index.js +160 -0
- package/dist/src/tokenEstimator.d.ts +87 -0
- package/dist/src/tokenEstimator.js +238 -0
- package/dist/src/types.d.ts +135 -0
- package/dist/src/types.js +12 -0
- package/dist/src/validateEnvelope.d.ts +15 -0
- package/dist/src/validateEnvelope.js +31 -0
- package/lafs.md +819 -0
- package/package.json +88 -0
- package/schemas/v1/agent-card.schema.json +230 -0
- package/schemas/v1/conformance-profiles.json +39 -0
- package/schemas/v1/context-ledger.schema.json +70 -0
- package/schemas/v1/discovery.schema.json +132 -0
- package/schemas/v1/envelope.schema.json +306 -0
- package/schemas/v1/error-registry.json +162 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { isRegisteredErrorCode, getRegistryCode, getAgentAction, getDocUrl } from "./errorRegistry.js";
|
|
2
|
+
import { assertEnvelope } from "./validateEnvelope.js";
|
|
3
|
+
export const LAFS_SCHEMA_URL = "https://lafs.dev/schemas/v1/envelope.schema.json";
|
|
4
|
+
function resolveMviLevel(input) {
|
|
5
|
+
if (typeof input === "boolean") {
|
|
6
|
+
return input ? "minimal" : "standard";
|
|
7
|
+
}
|
|
8
|
+
return input ?? "standard";
|
|
9
|
+
}
|
|
10
|
+
function createMeta(input) {
|
|
11
|
+
return {
|
|
12
|
+
specVersion: input.specVersion ?? "1.0.0",
|
|
13
|
+
schemaVersion: input.schemaVersion ?? "1.0.0",
|
|
14
|
+
timestamp: input.timestamp ?? new Date().toISOString(),
|
|
15
|
+
operation: input.operation,
|
|
16
|
+
requestId: input.requestId,
|
|
17
|
+
transport: input.transport ?? "sdk",
|
|
18
|
+
strict: input.strict ?? true,
|
|
19
|
+
mvi: resolveMviLevel(input.mvi),
|
|
20
|
+
contextVersion: input.contextVersion ?? 0,
|
|
21
|
+
...(input.sessionId ? { sessionId: input.sessionId } : {}),
|
|
22
|
+
...(input.warnings ? { warnings: input.warnings } : {}),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export const CATEGORY_ACTION_MAP = {
|
|
26
|
+
VALIDATION: 'retry_modified',
|
|
27
|
+
AUTH: 'authenticate',
|
|
28
|
+
PERMISSION: 'escalate',
|
|
29
|
+
NOT_FOUND: 'stop',
|
|
30
|
+
CONFLICT: 'retry_modified',
|
|
31
|
+
RATE_LIMIT: 'wait',
|
|
32
|
+
TRANSIENT: 'retry',
|
|
33
|
+
INTERNAL: 'escalate',
|
|
34
|
+
CONTRACT: 'retry_modified',
|
|
35
|
+
MIGRATION: 'stop',
|
|
36
|
+
};
|
|
37
|
+
function normalizeError(error) {
|
|
38
|
+
const registryEntry = getRegistryCode(error.code);
|
|
39
|
+
const category = (error.category ?? registryEntry?.category ?? "INTERNAL");
|
|
40
|
+
const retryable = error.retryable ?? registryEntry?.retryable ?? false;
|
|
41
|
+
// Derive agentAction: explicit > registry > category fallback
|
|
42
|
+
const agentAction = error.agentAction ??
|
|
43
|
+
getAgentAction(error.code) ??
|
|
44
|
+
CATEGORY_ACTION_MAP[category];
|
|
45
|
+
const docUrl = error.docUrl ?? getDocUrl(error.code);
|
|
46
|
+
const result = {
|
|
47
|
+
code: error.code,
|
|
48
|
+
message: error.message,
|
|
49
|
+
category,
|
|
50
|
+
retryable,
|
|
51
|
+
retryAfterMs: error.retryAfterMs ?? null,
|
|
52
|
+
details: error.details ?? {},
|
|
53
|
+
};
|
|
54
|
+
if (agentAction !== undefined) {
|
|
55
|
+
result.agentAction = agentAction;
|
|
56
|
+
}
|
|
57
|
+
if (error.escalationRequired !== undefined) {
|
|
58
|
+
result.escalationRequired = error.escalationRequired;
|
|
59
|
+
}
|
|
60
|
+
if (error.suggestedAction !== undefined) {
|
|
61
|
+
result.suggestedAction = error.suggestedAction;
|
|
62
|
+
}
|
|
63
|
+
if (docUrl !== undefined) {
|
|
64
|
+
result.docUrl = docUrl;
|
|
65
|
+
}
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
export function createEnvelope(input) {
|
|
69
|
+
const meta = createMeta(input.meta);
|
|
70
|
+
if (input.success) {
|
|
71
|
+
return {
|
|
72
|
+
$schema: LAFS_SCHEMA_URL,
|
|
73
|
+
_meta: meta,
|
|
74
|
+
success: true,
|
|
75
|
+
result: input.result,
|
|
76
|
+
...(input.page !== undefined ? { page: input.page } : {}),
|
|
77
|
+
...(input.error !== undefined ? { error: null } : {}),
|
|
78
|
+
...(input._extensions !== undefined ? { _extensions: input._extensions } : {}),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
$schema: LAFS_SCHEMA_URL,
|
|
83
|
+
_meta: meta,
|
|
84
|
+
success: false,
|
|
85
|
+
// Pass through result if provided — validation tools need actionable data
|
|
86
|
+
// alongside error metadata. Default to null for traditional error responses.
|
|
87
|
+
result: input.result ?? null,
|
|
88
|
+
error: normalizeError(input.error),
|
|
89
|
+
...(input.page !== undefined ? { page: input.page } : {}),
|
|
90
|
+
...(input._extensions !== undefined ? { _extensions: input._extensions } : {}),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
export class LafsError extends Error {
|
|
94
|
+
code;
|
|
95
|
+
category;
|
|
96
|
+
retryable;
|
|
97
|
+
retryAfterMs;
|
|
98
|
+
details;
|
|
99
|
+
registered;
|
|
100
|
+
agentAction;
|
|
101
|
+
escalationRequired;
|
|
102
|
+
suggestedAction;
|
|
103
|
+
docUrl;
|
|
104
|
+
constructor(error) {
|
|
105
|
+
super(error.message);
|
|
106
|
+
this.name = "LafsError";
|
|
107
|
+
this.code = error.code;
|
|
108
|
+
this.category = error.category;
|
|
109
|
+
this.retryable = error.retryable;
|
|
110
|
+
this.retryAfterMs = error.retryAfterMs;
|
|
111
|
+
this.details = error.details;
|
|
112
|
+
this.registered = isRegisteredErrorCode(error.code);
|
|
113
|
+
if (error.agentAction !== undefined)
|
|
114
|
+
this.agentAction = error.agentAction;
|
|
115
|
+
if (error.escalationRequired !== undefined)
|
|
116
|
+
this.escalationRequired = error.escalationRequired;
|
|
117
|
+
if (error.suggestedAction !== undefined)
|
|
118
|
+
this.suggestedAction = error.suggestedAction;
|
|
119
|
+
if (error.docUrl !== undefined)
|
|
120
|
+
this.docUrl = error.docUrl;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
export function parseLafsResponse(input, options = {}) {
|
|
124
|
+
const envelope = assertEnvelope(input);
|
|
125
|
+
if (envelope.success) {
|
|
126
|
+
return envelope.result;
|
|
127
|
+
}
|
|
128
|
+
const error = envelope.error;
|
|
129
|
+
if (!error) {
|
|
130
|
+
throw new Error("Invalid LAFS envelope: success=false requires error object");
|
|
131
|
+
}
|
|
132
|
+
if (options.requireRegisteredErrorCode && !isRegisteredErrorCode(error.code)) {
|
|
133
|
+
throw new Error(`Unregistered LAFS error code: ${error.code}`);
|
|
134
|
+
}
|
|
135
|
+
throw new LafsError(error);
|
|
136
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { LAFSAgentAction } from "./types.js";
|
|
2
|
+
export interface RegistryCode {
|
|
3
|
+
code: string;
|
|
4
|
+
category: string;
|
|
5
|
+
description: string;
|
|
6
|
+
retryable: boolean;
|
|
7
|
+
httpStatus: number;
|
|
8
|
+
grpcStatus: string;
|
|
9
|
+
cliExit: number;
|
|
10
|
+
agentAction?: string;
|
|
11
|
+
typeUri?: string;
|
|
12
|
+
docUrl?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface ErrorRegistry {
|
|
15
|
+
version: string;
|
|
16
|
+
codes: RegistryCode[];
|
|
17
|
+
}
|
|
18
|
+
export type TransportMapping = {
|
|
19
|
+
transport: "http" | "grpc" | "cli";
|
|
20
|
+
value: number | string;
|
|
21
|
+
};
|
|
22
|
+
export declare function getErrorRegistry(): ErrorRegistry;
|
|
23
|
+
export declare function isRegisteredErrorCode(code: string): boolean;
|
|
24
|
+
export declare function getRegistryCode(code: string): RegistryCode | undefined;
|
|
25
|
+
export declare function getAgentAction(code: string): LAFSAgentAction | undefined;
|
|
26
|
+
export declare function getTypeUri(code: string): string | undefined;
|
|
27
|
+
export declare function getDocUrl(code: string): string | undefined;
|
|
28
|
+
export declare function getTransportMapping(code: string, transport: "http" | "grpc" | "cli"): TransportMapping | null;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import errorRegistry from "../schemas/v1/error-registry.json" with { type: "json" };
|
|
2
|
+
export function getErrorRegistry() {
|
|
3
|
+
return errorRegistry;
|
|
4
|
+
}
|
|
5
|
+
export function isRegisteredErrorCode(code) {
|
|
6
|
+
const registry = getErrorRegistry();
|
|
7
|
+
return registry.codes.some((item) => item.code === code);
|
|
8
|
+
}
|
|
9
|
+
export function getRegistryCode(code) {
|
|
10
|
+
return getErrorRegistry().codes.find((item) => item.code === code);
|
|
11
|
+
}
|
|
12
|
+
export function getAgentAction(code) {
|
|
13
|
+
const entry = getRegistryCode(code);
|
|
14
|
+
return entry?.agentAction;
|
|
15
|
+
}
|
|
16
|
+
export function getTypeUri(code) {
|
|
17
|
+
const entry = getRegistryCode(code);
|
|
18
|
+
return entry?.typeUri;
|
|
19
|
+
}
|
|
20
|
+
export function getDocUrl(code) {
|
|
21
|
+
const entry = getRegistryCode(code);
|
|
22
|
+
return entry?.docUrl;
|
|
23
|
+
}
|
|
24
|
+
export function getTransportMapping(code, transport) {
|
|
25
|
+
const registryCode = getRegistryCode(code);
|
|
26
|
+
if (!registryCode) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
if (transport === "http") {
|
|
30
|
+
return { transport, value: registryCode.httpStatus };
|
|
31
|
+
}
|
|
32
|
+
if (transport === "grpc") {
|
|
33
|
+
return { transport, value: registryCode.grpcStatus };
|
|
34
|
+
}
|
|
35
|
+
return { transport, value: registryCode.cliExit };
|
|
36
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified cross-layer flag resolver.
|
|
3
|
+
*
|
|
4
|
+
* Composes format resolution (§5.1–5.3) with field extraction resolution (§9.2)
|
|
5
|
+
* and validates cross-layer interactions per §5.4.
|
|
6
|
+
*
|
|
7
|
+
* @since 1.6.0
|
|
8
|
+
*/
|
|
9
|
+
import { type FlagResolution } from './flagSemantics.js';
|
|
10
|
+
import { type FieldExtractionResolution } from './fieldExtraction.js';
|
|
11
|
+
/** Combined input for both format and field extraction layers. */
|
|
12
|
+
export interface UnifiedFlagInput {
|
|
13
|
+
human?: boolean;
|
|
14
|
+
json?: boolean;
|
|
15
|
+
quiet?: boolean;
|
|
16
|
+
requestedFormat?: 'json' | 'human';
|
|
17
|
+
projectDefault?: 'json' | 'human';
|
|
18
|
+
userDefault?: 'json' | 'human';
|
|
19
|
+
/**
|
|
20
|
+
* TTY detection hint. When true, defaults to human format if no
|
|
21
|
+
* explicit format flag or project/user default is set.
|
|
22
|
+
* CLI tools should pass `process.stdout.isTTY ?? false`.
|
|
23
|
+
*/
|
|
24
|
+
tty?: boolean;
|
|
25
|
+
field?: string;
|
|
26
|
+
fields?: string | string[];
|
|
27
|
+
mvi?: string;
|
|
28
|
+
}
|
|
29
|
+
/** Combined resolution result with cross-layer warnings. */
|
|
30
|
+
export interface UnifiedFlagResolution {
|
|
31
|
+
/** Resolved format layer. */
|
|
32
|
+
format: FlagResolution;
|
|
33
|
+
/** Resolved field extraction layer. */
|
|
34
|
+
fields: FieldExtractionResolution;
|
|
35
|
+
/** Warnings for cross-layer interactions (non-fatal). */
|
|
36
|
+
warnings: string[];
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Resolve all flags across both layers and validate cross-layer semantics.
|
|
40
|
+
*
|
|
41
|
+
* Per §5.4, cross-layer combinations are valid but MAY produce warnings.
|
|
42
|
+
* Format-layer conflicts (E_FORMAT_CONFLICT) and field-layer conflicts
|
|
43
|
+
* (E_FIELD_CONFLICT) still throw as before — they are delegated to the
|
|
44
|
+
* existing single-layer resolvers.
|
|
45
|
+
*/
|
|
46
|
+
export declare function resolveFlags(input: UnifiedFlagInput): UnifiedFlagResolution;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified cross-layer flag resolver.
|
|
3
|
+
*
|
|
4
|
+
* Composes format resolution (§5.1–5.3) with field extraction resolution (§9.2)
|
|
5
|
+
* and validates cross-layer interactions per §5.4.
|
|
6
|
+
*
|
|
7
|
+
* @since 1.6.0
|
|
8
|
+
*/
|
|
9
|
+
import { resolveOutputFormat } from './flagSemantics.js';
|
|
10
|
+
import { resolveFieldExtraction } from './fieldExtraction.js';
|
|
11
|
+
/**
|
|
12
|
+
* Resolve all flags across both layers and validate cross-layer semantics.
|
|
13
|
+
*
|
|
14
|
+
* Per §5.4, cross-layer combinations are valid but MAY produce warnings.
|
|
15
|
+
* Format-layer conflicts (E_FORMAT_CONFLICT) and field-layer conflicts
|
|
16
|
+
* (E_FIELD_CONFLICT) still throw as before — they are delegated to the
|
|
17
|
+
* existing single-layer resolvers.
|
|
18
|
+
*/
|
|
19
|
+
export function resolveFlags(input) {
|
|
20
|
+
const formatInput = {
|
|
21
|
+
humanFlag: input.human,
|
|
22
|
+
jsonFlag: input.json,
|
|
23
|
+
quiet: input.quiet,
|
|
24
|
+
requestedFormat: input.requestedFormat,
|
|
25
|
+
projectDefault: input.projectDefault,
|
|
26
|
+
userDefault: input.userDefault,
|
|
27
|
+
tty: input.tty,
|
|
28
|
+
};
|
|
29
|
+
const format = resolveOutputFormat(formatInput);
|
|
30
|
+
const fieldInput = {
|
|
31
|
+
fieldFlag: input.field,
|
|
32
|
+
fieldsFlag: input.fields,
|
|
33
|
+
mviFlag: input.mvi,
|
|
34
|
+
};
|
|
35
|
+
const fields = resolveFieldExtraction(fieldInput);
|
|
36
|
+
// Cross-layer validation (§5.4)
|
|
37
|
+
const warnings = [];
|
|
38
|
+
if (format.format === 'human' && fields.field) {
|
|
39
|
+
warnings.push(`Cross-layer: --human + --field "${fields.field}". ` +
|
|
40
|
+
'Field extraction applies first, then human rendering (§5.4.1).');
|
|
41
|
+
}
|
|
42
|
+
if (format.format === 'human' && fields.fields && fields.fields.length > 0) {
|
|
43
|
+
warnings.push(`Cross-layer: --human + --fields [${fields.fields.join(', ')}]. ` +
|
|
44
|
+
'Field filtering applies first, then human rendering (§5.4.1).');
|
|
45
|
+
}
|
|
46
|
+
return { format, fields, warnings };
|
|
47
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { FlagInput, LAFSError, LAFSErrorCategory } from "./types.js";
|
|
2
|
+
export interface FlagResolution {
|
|
3
|
+
format: "json" | "human";
|
|
4
|
+
source: "flag" | "project" | "user" | "default";
|
|
5
|
+
/** When true, suppress non-essential output for scripting */
|
|
6
|
+
quiet: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare class LAFSFlagError extends Error implements LAFSError {
|
|
9
|
+
code: 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>);
|
|
15
|
+
}
|
|
16
|
+
export declare function resolveOutputFormat(input: FlagInput): FlagResolution;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { getRegistryCode } from "./errorRegistry.js";
|
|
2
|
+
export class LAFSFlagError extends Error {
|
|
3
|
+
code;
|
|
4
|
+
category;
|
|
5
|
+
retryable;
|
|
6
|
+
retryAfterMs;
|
|
7
|
+
details;
|
|
8
|
+
constructor(code, message, details = {}) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "LAFSFlagError";
|
|
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;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export function resolveOutputFormat(input) {
|
|
20
|
+
if (input.humanFlag && input.jsonFlag) {
|
|
21
|
+
throw new LAFSFlagError("E_FORMAT_CONFLICT", "Cannot combine --human and --json in the same invocation.");
|
|
22
|
+
}
|
|
23
|
+
const quiet = input.quiet ?? false;
|
|
24
|
+
if (input.requestedFormat) {
|
|
25
|
+
return { format: input.requestedFormat, source: "flag", quiet };
|
|
26
|
+
}
|
|
27
|
+
if (input.humanFlag) {
|
|
28
|
+
return { format: "human", source: "flag", quiet };
|
|
29
|
+
}
|
|
30
|
+
if (input.jsonFlag) {
|
|
31
|
+
return { format: "json", source: "flag", quiet };
|
|
32
|
+
}
|
|
33
|
+
if (input.projectDefault) {
|
|
34
|
+
return { format: input.projectDefault, source: "project", quiet };
|
|
35
|
+
}
|
|
36
|
+
if (input.userDefault) {
|
|
37
|
+
return { format: input.userDefault, source: "user", quiet };
|
|
38
|
+
}
|
|
39
|
+
// TTY terminals default to human-readable output for usability.
|
|
40
|
+
// Non-TTY (piped, CI, agents) defaults to JSON per LAFS protocol.
|
|
41
|
+
if (input.tty) {
|
|
42
|
+
return { format: "human", source: "default", quiet };
|
|
43
|
+
}
|
|
44
|
+
return { format: "json", source: "default", quiet };
|
|
45
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LAFS Health Check Module
|
|
3
|
+
*
|
|
4
|
+
* Provides health check endpoints for monitoring and orchestration
|
|
5
|
+
*/
|
|
6
|
+
export interface HealthCheckConfig {
|
|
7
|
+
path?: string;
|
|
8
|
+
checks?: HealthCheckFunction[];
|
|
9
|
+
}
|
|
10
|
+
export type HealthCheckFunction = () => Promise<HealthCheckResult> | HealthCheckResult;
|
|
11
|
+
export interface HealthCheckResult {
|
|
12
|
+
name: string;
|
|
13
|
+
status: 'ok' | 'warning' | 'error';
|
|
14
|
+
message?: string;
|
|
15
|
+
duration?: number;
|
|
16
|
+
}
|
|
17
|
+
export interface HealthStatus {
|
|
18
|
+
status: 'healthy' | 'degraded' | 'unhealthy';
|
|
19
|
+
timestamp: string;
|
|
20
|
+
version: string;
|
|
21
|
+
uptime: number;
|
|
22
|
+
checks: HealthCheckResult[];
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Health check middleware for Express applications
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```typescript
|
|
29
|
+
* import express from 'express';
|
|
30
|
+
* import { healthCheck } from '@cleocode/lafs/health';
|
|
31
|
+
*
|
|
32
|
+
* const app = express();
|
|
33
|
+
*
|
|
34
|
+
* // Basic health check
|
|
35
|
+
* app.use('/health', healthCheck());
|
|
36
|
+
*
|
|
37
|
+
* // Custom health checks
|
|
38
|
+
* app.use('/health', healthCheck({
|
|
39
|
+
* checks: [
|
|
40
|
+
* async () => ({
|
|
41
|
+
* name: 'database',
|
|
42
|
+
* status: await checkDatabase() ? 'ok' : 'error'
|
|
43
|
+
* })
|
|
44
|
+
* ]
|
|
45
|
+
* }));
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export declare function healthCheck(config?: HealthCheckConfig): (req: any, res: any) => Promise<void>;
|
|
49
|
+
/**
|
|
50
|
+
* Create a database health check
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```typescript
|
|
54
|
+
* const dbCheck = createDatabaseHealthCheck({
|
|
55
|
+
* checkConnection: async () => await db.ping()
|
|
56
|
+
* });
|
|
57
|
+
*
|
|
58
|
+
* app.use('/health', healthCheck({
|
|
59
|
+
* checks: [dbCheck]
|
|
60
|
+
* }));
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
export declare function createDatabaseHealthCheck(config: {
|
|
64
|
+
checkConnection: () => Promise<boolean>;
|
|
65
|
+
name?: string;
|
|
66
|
+
}): HealthCheckFunction;
|
|
67
|
+
/**
|
|
68
|
+
* Create an external service health check
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* ```typescript
|
|
72
|
+
* const apiCheck = createExternalServiceHealthCheck({
|
|
73
|
+
* name: 'payment-api',
|
|
74
|
+
* url: 'https://api.payment.com/health',
|
|
75
|
+
* timeout: 5000
|
|
76
|
+
* });
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
export declare function createExternalServiceHealthCheck(config: {
|
|
80
|
+
name: string;
|
|
81
|
+
url: string;
|
|
82
|
+
timeout?: number;
|
|
83
|
+
}): HealthCheckFunction;
|
|
84
|
+
/**
|
|
85
|
+
* Liveness probe - basic check that service is running
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* ```typescript
|
|
89
|
+
* app.get('/health/live', livenessProbe());
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
export declare function livenessProbe(): (req: any, res: any) => void;
|
|
93
|
+
/**
|
|
94
|
+
* Readiness probe - check that service is ready to accept traffic
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* ```typescript
|
|
98
|
+
* app.get('/health/ready', readinessProbe({
|
|
99
|
+
* checks: [dbCheck, cacheCheck]
|
|
100
|
+
* }));
|
|
101
|
+
* ```
|
|
102
|
+
*/
|
|
103
|
+
export declare function readinessProbe(config?: {
|
|
104
|
+
checks?: HealthCheckFunction[];
|
|
105
|
+
}): (req: any, res: any) => Promise<void>;
|