@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.
Files changed (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +235 -0
  3. package/dist/schemas/v1/conformance-profiles.json +39 -0
  4. package/dist/schemas/v1/envelope.schema.json +306 -0
  5. package/dist/schemas/v1/error-registry.json +162 -0
  6. package/dist/src/a2a/bindings/grpc.d.ts +67 -0
  7. package/dist/src/a2a/bindings/grpc.js +148 -0
  8. package/dist/src/a2a/bindings/http.d.ts +102 -0
  9. package/dist/src/a2a/bindings/http.js +120 -0
  10. package/dist/src/a2a/bindings/index.d.ts +35 -0
  11. package/dist/src/a2a/bindings/index.js +79 -0
  12. package/dist/src/a2a/bindings/jsonrpc.d.ts +77 -0
  13. package/dist/src/a2a/bindings/jsonrpc.js +114 -0
  14. package/dist/src/a2a/bridge.d.ts +175 -0
  15. package/dist/src/a2a/bridge.js +286 -0
  16. package/dist/src/a2a/extensions.d.ts +121 -0
  17. package/dist/src/a2a/extensions.js +205 -0
  18. package/dist/src/a2a/index.d.ts +40 -0
  19. package/dist/src/a2a/index.js +76 -0
  20. package/dist/src/a2a/streaming.d.ts +74 -0
  21. package/dist/src/a2a/streaming.js +265 -0
  22. package/dist/src/a2a/task-lifecycle.d.ts +109 -0
  23. package/dist/src/a2a/task-lifecycle.js +313 -0
  24. package/dist/src/budgetEnforcement.d.ts +84 -0
  25. package/dist/src/budgetEnforcement.js +328 -0
  26. package/dist/src/circuit-breaker/index.d.ts +121 -0
  27. package/dist/src/circuit-breaker/index.js +249 -0
  28. package/dist/src/cli.d.ts +16 -0
  29. package/dist/src/cli.js +63 -0
  30. package/dist/src/compliance.d.ts +31 -0
  31. package/dist/src/compliance.js +89 -0
  32. package/dist/src/conformance.d.ts +7 -0
  33. package/dist/src/conformance.js +248 -0
  34. package/dist/src/conformanceProfiles.d.ts +11 -0
  35. package/dist/src/conformanceProfiles.js +34 -0
  36. package/dist/src/deprecationRegistry.d.ts +13 -0
  37. package/dist/src/deprecationRegistry.js +39 -0
  38. package/dist/src/discovery.d.ts +286 -0
  39. package/dist/src/discovery.js +350 -0
  40. package/dist/src/envelope.d.ts +60 -0
  41. package/dist/src/envelope.js +136 -0
  42. package/dist/src/errorRegistry.d.ts +28 -0
  43. package/dist/src/errorRegistry.js +36 -0
  44. package/dist/src/fieldExtraction.d.ts +67 -0
  45. package/dist/src/fieldExtraction.js +133 -0
  46. package/dist/src/flagResolver.d.ts +46 -0
  47. package/dist/src/flagResolver.js +47 -0
  48. package/dist/src/flagSemantics.d.ts +16 -0
  49. package/dist/src/flagSemantics.js +45 -0
  50. package/dist/src/health/index.d.ts +105 -0
  51. package/dist/src/health/index.js +220 -0
  52. package/dist/src/index.d.ts +24 -0
  53. package/dist/src/index.js +34 -0
  54. package/dist/src/mcpAdapter.d.ts +28 -0
  55. package/dist/src/mcpAdapter.js +281 -0
  56. package/dist/src/mviProjection.d.ts +19 -0
  57. package/dist/src/mviProjection.js +116 -0
  58. package/dist/src/problemDetails.d.ts +34 -0
  59. package/dist/src/problemDetails.js +45 -0
  60. package/dist/src/shutdown/index.d.ts +69 -0
  61. package/dist/src/shutdown/index.js +160 -0
  62. package/dist/src/tokenEstimator.d.ts +87 -0
  63. package/dist/src/tokenEstimator.js +238 -0
  64. package/dist/src/types.d.ts +135 -0
  65. package/dist/src/types.js +12 -0
  66. package/dist/src/validateEnvelope.d.ts +15 -0
  67. package/dist/src/validateEnvelope.js +31 -0
  68. package/lafs.md +819 -0
  69. package/package.json +88 -0
  70. package/schemas/v1/agent-card.schema.json +230 -0
  71. package/schemas/v1/conformance-profiles.json +39 -0
  72. package/schemas/v1/context-ledger.schema.json +70 -0
  73. package/schemas/v1/discovery.schema.json +132 -0
  74. package/schemas/v1/envelope.schema.json +306 -0
  75. 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>;