@cleocode/lafs-protocol 1.3.2 → 1.4.1
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 +31 -4
- package/dist/schemas/v1/conformance-profiles.json +35 -0
- package/dist/src/a2a/bindings/index.d.ts +4 -0
- package/dist/src/a2a/bindings/index.js +25 -0
- package/dist/src/a2a/bridge.d.ts +2 -1
- package/dist/src/a2a/bridge.js +17 -0
- package/dist/src/a2a/extensions.d.ts +17 -0
- package/dist/src/a2a/extensions.js +43 -1
- package/dist/src/a2a/index.d.ts +3 -1
- package/dist/src/a2a/index.js +8 -2
- 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 +11 -0
- package/dist/src/a2a/task-lifecycle.js +52 -2
- package/dist/src/compliance.d.ts +31 -0
- package/dist/src/compliance.js +89 -0
- package/dist/src/conformance.d.ts +5 -1
- package/dist/src/conformance.js +103 -3
- 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/envelope.d.ts +46 -0
- package/dist/src/envelope.js +89 -0
- package/dist/src/errorRegistry.d.ts +6 -0
- package/dist/src/errorRegistry.js +16 -0
- package/dist/src/index.d.ts +6 -2
- package/dist/src/index.js +8 -2
- package/lafs.md +15 -3
- package/package.json +7 -2
- package/schemas/v1/conformance-profiles.json +35 -0
|
@@ -38,10 +38,17 @@ export declare class TaskNotFoundError extends Error {
|
|
|
38
38
|
readonly taskId: string;
|
|
39
39
|
constructor(taskId: string);
|
|
40
40
|
}
|
|
41
|
+
/** Thrown when a refinement/follow-up task references invalid parent tasks */
|
|
42
|
+
export declare class TaskRefinementError extends Error {
|
|
43
|
+
readonly referenceTaskIds: string[];
|
|
44
|
+
constructor(message: string, referenceTaskIds: string[]);
|
|
45
|
+
}
|
|
41
46
|
/** Options for creating a new task */
|
|
42
47
|
export interface CreateTaskOptions {
|
|
43
48
|
contextId?: string;
|
|
44
49
|
metadata?: Record<string, unknown>;
|
|
50
|
+
referenceTaskIds?: string[];
|
|
51
|
+
parallelFollowUp?: boolean;
|
|
45
52
|
}
|
|
46
53
|
/** Options for listing tasks */
|
|
47
54
|
export interface ListTasksOptions {
|
|
@@ -64,6 +71,8 @@ export declare class TaskManager {
|
|
|
64
71
|
private contextIndex;
|
|
65
72
|
/** Create a new task in the submitted state */
|
|
66
73
|
createTask(options?: CreateTaskOptions): Task;
|
|
74
|
+
/** Create a refinement/follow-up task referencing existing task(s). */
|
|
75
|
+
createRefinedTask(referenceTaskIds: string[], options?: Omit<CreateTaskOptions, 'referenceTaskIds'>): Task;
|
|
67
76
|
/** Get a task by ID. Throws TaskNotFoundError if not found. */
|
|
68
77
|
getTask(taskId: string): Task;
|
|
69
78
|
/** List tasks with optional filtering and pagination */
|
|
@@ -90,6 +99,8 @@ export declare class TaskManager {
|
|
|
90
99
|
getTasksByContext(contextId: string): Task[];
|
|
91
100
|
/** Check if a task is in a terminal state */
|
|
92
101
|
isTerminal(taskId: string): boolean;
|
|
102
|
+
private resolveContextForReferenceTasks;
|
|
103
|
+
private validateReferenceTasks;
|
|
93
104
|
}
|
|
94
105
|
/**
|
|
95
106
|
* Attach a LAFS envelope as an artifact to an A2A task.
|
|
@@ -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
|
|
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
|
-
...(
|
|
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
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ConformanceReport, FlagInput, LAFSEnvelope } from "./types.js";
|
|
2
|
+
import { type EnvelopeValidationResult } from "./validateEnvelope.js";
|
|
3
|
+
export type ComplianceStage = "schema" | "envelope" | "flags" | "format";
|
|
4
|
+
export interface ComplianceIssue {
|
|
5
|
+
stage: ComplianceStage;
|
|
6
|
+
message: string;
|
|
7
|
+
detail?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface EnforceComplianceOptions {
|
|
10
|
+
checkConformance?: boolean;
|
|
11
|
+
checkFlags?: boolean;
|
|
12
|
+
flags?: FlagInput;
|
|
13
|
+
requireJsonOutput?: boolean;
|
|
14
|
+
}
|
|
15
|
+
export interface ComplianceResult {
|
|
16
|
+
ok: boolean;
|
|
17
|
+
envelope?: LAFSEnvelope;
|
|
18
|
+
validation: EnvelopeValidationResult;
|
|
19
|
+
envelopeConformance?: ConformanceReport;
|
|
20
|
+
flagConformance?: ConformanceReport;
|
|
21
|
+
issues: ComplianceIssue[];
|
|
22
|
+
}
|
|
23
|
+
export declare class ComplianceError extends Error {
|
|
24
|
+
readonly issues: ComplianceIssue[];
|
|
25
|
+
constructor(issues: ComplianceIssue[]);
|
|
26
|
+
}
|
|
27
|
+
export declare function enforceCompliance(input: unknown, options?: EnforceComplianceOptions): ComplianceResult;
|
|
28
|
+
export declare function assertCompliance(input: unknown, options?: EnforceComplianceOptions): LAFSEnvelope;
|
|
29
|
+
export declare function withCompliance<TArgs extends unknown[], TResult extends LAFSEnvelope>(producer: (...args: TArgs) => TResult | Promise<TResult>, options?: EnforceComplianceOptions): (...args: TArgs) => Promise<LAFSEnvelope>;
|
|
30
|
+
export type ComplianceMiddleware = (envelope: LAFSEnvelope, next: () => LAFSEnvelope | Promise<LAFSEnvelope>) => Promise<LAFSEnvelope> | LAFSEnvelope;
|
|
31
|
+
export declare function createComplianceMiddleware(options?: EnforceComplianceOptions): ComplianceMiddleware;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { runEnvelopeConformance, runFlagConformance } from "./conformance.js";
|
|
2
|
+
import { resolveOutputFormat } from "./flagSemantics.js";
|
|
3
|
+
import { assertEnvelope, validateEnvelope } from "./validateEnvelope.js";
|
|
4
|
+
export class ComplianceError extends Error {
|
|
5
|
+
issues;
|
|
6
|
+
constructor(issues) {
|
|
7
|
+
super(`LAFS compliance failed: ${issues.map((issue) => issue.message).join("; ")}`);
|
|
8
|
+
this.name = "ComplianceError";
|
|
9
|
+
this.issues = issues;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function conformanceIssues(report, stage) {
|
|
13
|
+
return report.checks
|
|
14
|
+
.filter((check) => !check.pass)
|
|
15
|
+
.map((check) => ({
|
|
16
|
+
stage,
|
|
17
|
+
message: `${check.name} failed`,
|
|
18
|
+
detail: check.detail,
|
|
19
|
+
}));
|
|
20
|
+
}
|
|
21
|
+
export function enforceCompliance(input, options = {}) {
|
|
22
|
+
const { checkConformance = true, checkFlags = false, flags, requireJsonOutput = false, } = options;
|
|
23
|
+
const issues = [];
|
|
24
|
+
const validation = validateEnvelope(input);
|
|
25
|
+
if (!validation.valid) {
|
|
26
|
+
issues.push(...validation.errors.map((error) => ({
|
|
27
|
+
stage: "schema",
|
|
28
|
+
message: "schema validation failed",
|
|
29
|
+
detail: error,
|
|
30
|
+
})));
|
|
31
|
+
return {
|
|
32
|
+
ok: false,
|
|
33
|
+
validation,
|
|
34
|
+
issues,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
const envelope = assertEnvelope(input);
|
|
38
|
+
let envelopeConformance;
|
|
39
|
+
if (checkConformance) {
|
|
40
|
+
envelopeConformance = runEnvelopeConformance(envelope);
|
|
41
|
+
if (!envelopeConformance.ok) {
|
|
42
|
+
issues.push(...conformanceIssues(envelopeConformance, "envelope"));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
let flagConformance;
|
|
46
|
+
if (checkFlags && flags) {
|
|
47
|
+
flagConformance = runFlagConformance(flags);
|
|
48
|
+
if (!flagConformance.ok) {
|
|
49
|
+
issues.push(...conformanceIssues(flagConformance, "flags"));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (requireJsonOutput) {
|
|
53
|
+
const resolved = resolveOutputFormat(flags ?? {});
|
|
54
|
+
if (resolved.format !== "json") {
|
|
55
|
+
issues.push({
|
|
56
|
+
stage: "format",
|
|
57
|
+
message: "non-json output format resolved",
|
|
58
|
+
detail: `resolved format is ${resolved.format}`,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
ok: issues.length === 0,
|
|
64
|
+
envelope,
|
|
65
|
+
validation,
|
|
66
|
+
envelopeConformance,
|
|
67
|
+
flagConformance,
|
|
68
|
+
issues,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
export function assertCompliance(input, options = {}) {
|
|
72
|
+
const result = enforceCompliance(input, options);
|
|
73
|
+
if (!result.ok || !result.envelope) {
|
|
74
|
+
throw new ComplianceError(result.issues);
|
|
75
|
+
}
|
|
76
|
+
return result.envelope;
|
|
77
|
+
}
|
|
78
|
+
export function withCompliance(producer, options = {}) {
|
|
79
|
+
return async (...args) => {
|
|
80
|
+
const envelope = await producer(...args);
|
|
81
|
+
return assertCompliance(envelope, options);
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
export function createComplianceMiddleware(options = {}) {
|
|
85
|
+
return async (_envelope, next) => {
|
|
86
|
+
const candidate = await next();
|
|
87
|
+
return assertCompliance(candidate, options);
|
|
88
|
+
};
|
|
89
|
+
}
|
|
@@ -1,3 +1,7 @@
|
|
|
1
1
|
import type { ConformanceReport, FlagInput } from "./types.js";
|
|
2
|
-
|
|
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;
|
package/dist/src/conformance.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { isRegisteredErrorCode } from "./errorRegistry.js";
|
|
1
|
+
import { getTransportMapping, isRegisteredErrorCode } from "./errorRegistry.js";
|
|
2
2
|
import { resolveOutputFormat, LAFSFlagError } from "./flagSemantics.js";
|
|
3
|
+
import { getChecksForTier } from "./conformanceProfiles.js";
|
|
3
4
|
import { validateEnvelope } from "./validateEnvelope.js";
|
|
4
5
|
function pushCheck(checks, name, pass, detail) {
|
|
5
6
|
checks.push({ name, pass, ...(detail ? { detail } : {}) });
|
|
6
7
|
}
|
|
7
|
-
export function runEnvelopeConformance(envelope) {
|
|
8
|
+
export function runEnvelopeConformance(envelope, options = {}) {
|
|
8
9
|
const checks = [];
|
|
9
10
|
const validation = validateEnvelope(envelope);
|
|
10
11
|
pushCheck(checks, "envelope_schema_valid", validation.valid, validation.valid ? undefined : validation.errors.join("; "));
|
|
@@ -30,6 +31,50 @@ export function runEnvelopeConformance(envelope) {
|
|
|
30
31
|
else {
|
|
31
32
|
pushCheck(checks, "error_code_registered", true, "error field absent or null — skipped (optional when success=true)");
|
|
32
33
|
}
|
|
34
|
+
// transport_mapping_consistent: when an error is present, ensure the code has
|
|
35
|
+
// a transport-specific mapping in the registry for the declared transport.
|
|
36
|
+
if (typed.error) {
|
|
37
|
+
if (typed._meta.transport === "sdk") {
|
|
38
|
+
pushCheck(checks, "transport_mapping_consistent", true, "sdk transport does not require external status-code mapping");
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
const mapping = getTransportMapping(typed.error.code, typed._meta.transport);
|
|
42
|
+
const mappingOk = mapping !== null;
|
|
43
|
+
pushCheck(checks, "transport_mapping_consistent", mappingOk, mappingOk
|
|
44
|
+
? undefined
|
|
45
|
+
: `no ${typed._meta.transport} mapping found for code ${typed.error.code}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
pushCheck(checks, "transport_mapping_consistent", true, "no error present — mapping check skipped");
|
|
50
|
+
}
|
|
51
|
+
// context_mutation_failure: if the producer marks context as required for a
|
|
52
|
+
// mutation operation, missing context must fail with a context error code.
|
|
53
|
+
{
|
|
54
|
+
const ext = (typed._extensions ?? {});
|
|
55
|
+
const contextObj = (ext["context"] ?? {});
|
|
56
|
+
const lafsObj = (ext["lafs"] ?? {});
|
|
57
|
+
const contextRequired = ext["lafsContextRequired"] === true ||
|
|
58
|
+
contextObj["required"] === true ||
|
|
59
|
+
lafsObj["contextRequired"] === true;
|
|
60
|
+
if (!contextRequired) {
|
|
61
|
+
pushCheck(checks, "context_mutation_failure", true, "context not marked required — skipped");
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
const hasContextIdentity = typed._meta.contextVersion > 0 || Boolean(typed._meta.sessionId);
|
|
65
|
+
if (typed.success) {
|
|
66
|
+
const pass = hasContextIdentity;
|
|
67
|
+
pushCheck(checks, "context_mutation_failure", pass, pass ? undefined : "context required but missing identity (expect E_CONTEXT_MISSING)");
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
const code = typed.error?.code;
|
|
71
|
+
const pass = code === "E_CONTEXT_MISSING" || code === "E_CONTEXT_STALE";
|
|
72
|
+
pushCheck(checks, "context_mutation_failure", pass, pass
|
|
73
|
+
? undefined
|
|
74
|
+
: `context required failures should return E_CONTEXT_MISSING or E_CONTEXT_STALE, got ${String(code)}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
33
78
|
const validMviLevels = ["minimal", "standard", "full", "custom"];
|
|
34
79
|
pushCheck(checks, "meta_mvi_present", validMviLevels.includes(typed._meta.mvi), validMviLevels.includes(typed._meta.mvi) ? undefined : `invalid mvi level: ${String(typed._meta.mvi)}`);
|
|
35
80
|
pushCheck(checks, "meta_strict_present", typeof typed._meta.strict === "boolean");
|
|
@@ -72,6 +117,9 @@ export function runEnvelopeConformance(envelope) {
|
|
|
72
117
|
}
|
|
73
118
|
pushCheck(checks, "pagination_mode_consistent", consistent, consistent ? undefined : detail);
|
|
74
119
|
}
|
|
120
|
+
else {
|
|
121
|
+
pushCheck(checks, "pagination_mode_consistent", true, "page absent — skipped");
|
|
122
|
+
}
|
|
75
123
|
// strict_mode_enforced: verify the schema enforces additional-property rules.
|
|
76
124
|
// When strict=true, extra top-level properties must be rejected by validation.
|
|
77
125
|
// When strict=false, extra top-level properties must be allowed.
|
|
@@ -85,7 +133,59 @@ export function runEnvelopeConformance(envelope) {
|
|
|
85
133
|
pushCheck(checks, "strict_mode_enforced", extraResult.valid, !extraResult.valid ? "strict=false but additional properties were rejected" : undefined);
|
|
86
134
|
}
|
|
87
135
|
}
|
|
88
|
-
|
|
136
|
+
// context_preservation_valid: validate monotonic context version behavior and
|
|
137
|
+
// context-constraint integrity when a context ledger extension is present.
|
|
138
|
+
{
|
|
139
|
+
const ext = (typed._extensions ?? {});
|
|
140
|
+
const ledger = (ext["contextLedger"] ?? ext["context"]);
|
|
141
|
+
if (!ledger || typeof ledger !== "object") {
|
|
142
|
+
pushCheck(checks, "context_preservation_valid", true, "context ledger absent — skipped");
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
const version = ledger["version"];
|
|
146
|
+
const previousVersion = ledger["previousVersion"];
|
|
147
|
+
const removedConstraints = ledger["removedConstraints"];
|
|
148
|
+
const hasNumericVersion = typeof version === "number";
|
|
149
|
+
const matchesEnvelopeVersion = hasNumericVersion && version === typed._meta.contextVersion;
|
|
150
|
+
const monotonicFromPrevious = typeof previousVersion !== "number" || (hasNumericVersion && version >= previousVersion);
|
|
151
|
+
const constraintsPreserved = !Array.isArray(removedConstraints) || removedConstraints.length === 0 || !typed.success;
|
|
152
|
+
let pass = matchesEnvelopeVersion && monotonicFromPrevious && constraintsPreserved;
|
|
153
|
+
let detail;
|
|
154
|
+
if (!hasNumericVersion) {
|
|
155
|
+
pass = false;
|
|
156
|
+
detail = "context ledger version must be numeric";
|
|
157
|
+
}
|
|
158
|
+
else if (!matchesEnvelopeVersion) {
|
|
159
|
+
detail = `context version mismatch: ledger=${String(version)} envelope=${typed._meta.contextVersion}`;
|
|
160
|
+
}
|
|
161
|
+
else if (!monotonicFromPrevious) {
|
|
162
|
+
detail = `non-monotonic context version: previous=${String(previousVersion)} current=${String(version)}`;
|
|
163
|
+
}
|
|
164
|
+
else if (!constraintsPreserved) {
|
|
165
|
+
detail = "context constraint removal detected on successful response";
|
|
166
|
+
}
|
|
167
|
+
// Error-path validation for stale/missing context signaling.
|
|
168
|
+
if (!typed.success && typed.error && ledger["required"] === true) {
|
|
169
|
+
const stale = ledger["stale"] === true;
|
|
170
|
+
if (stale && typed.error.code !== "E_CONTEXT_STALE") {
|
|
171
|
+
pass = false;
|
|
172
|
+
detail = `stale context should return E_CONTEXT_STALE, got ${typed.error.code}`;
|
|
173
|
+
}
|
|
174
|
+
if (!stale && typed.error.code !== "E_CONTEXT_MISSING" && typed.error.code !== "E_CONTEXT_STALE") {
|
|
175
|
+
pass = false;
|
|
176
|
+
detail = `required context failure should return E_CONTEXT_MISSING or E_CONTEXT_STALE, got ${typed.error.code}`;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
pushCheck(checks, "context_preservation_valid", pass, detail);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
const tier = options.tier;
|
|
183
|
+
if (!tier) {
|
|
184
|
+
return { ok: checks.every((check) => check.pass), checks };
|
|
185
|
+
}
|
|
186
|
+
const allowed = new Set(getChecksForTier(tier));
|
|
187
|
+
const tierChecks = checks.filter((check) => allowed.has(check.name));
|
|
188
|
+
return { ok: tierChecks.every((check) => check.pass), checks: tierChecks };
|
|
89
189
|
}
|
|
90
190
|
export function runFlagConformance(flags) {
|
|
91
191
|
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
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { LAFSEnvelope, LAFSError, LAFSErrorCategory, LAFSMeta, LAFSTransport, MVILevel } from "./types.js";
|
|
2
|
+
export declare const LAFS_SCHEMA_URL: "https://lafs.dev/schemas/v1/envelope.schema.json";
|
|
3
|
+
export interface CreateEnvelopeMetaInput {
|
|
4
|
+
operation: string;
|
|
5
|
+
requestId: string;
|
|
6
|
+
transport?: LAFSTransport;
|
|
7
|
+
specVersion?: string;
|
|
8
|
+
schemaVersion?: string;
|
|
9
|
+
timestamp?: string;
|
|
10
|
+
strict?: boolean;
|
|
11
|
+
mvi?: MVILevel | boolean;
|
|
12
|
+
contextVersion?: number;
|
|
13
|
+
sessionId?: string;
|
|
14
|
+
warnings?: LAFSMeta["warnings"];
|
|
15
|
+
}
|
|
16
|
+
export interface CreateEnvelopeSuccessInput {
|
|
17
|
+
success: true;
|
|
18
|
+
result: LAFSEnvelope["result"];
|
|
19
|
+
page?: LAFSEnvelope["page"];
|
|
20
|
+
error?: null;
|
|
21
|
+
_extensions?: LAFSEnvelope["_extensions"];
|
|
22
|
+
meta: CreateEnvelopeMetaInput;
|
|
23
|
+
}
|
|
24
|
+
export interface CreateEnvelopeErrorInput {
|
|
25
|
+
success: false;
|
|
26
|
+
error: Partial<LAFSError> & Pick<LAFSError, "code" | "message">;
|
|
27
|
+
result?: null;
|
|
28
|
+
page?: LAFSEnvelope["page"];
|
|
29
|
+
_extensions?: LAFSEnvelope["_extensions"];
|
|
30
|
+
meta: CreateEnvelopeMetaInput;
|
|
31
|
+
}
|
|
32
|
+
export type CreateEnvelopeInput = CreateEnvelopeSuccessInput | CreateEnvelopeErrorInput;
|
|
33
|
+
export declare function createEnvelope(input: CreateEnvelopeInput): LAFSEnvelope;
|
|
34
|
+
export declare class LafsError extends Error implements LAFSError {
|
|
35
|
+
code: string;
|
|
36
|
+
category: LAFSErrorCategory;
|
|
37
|
+
retryable: boolean;
|
|
38
|
+
retryAfterMs: number | null;
|
|
39
|
+
details: Record<string, unknown>;
|
|
40
|
+
registered: boolean;
|
|
41
|
+
constructor(error: LAFSError);
|
|
42
|
+
}
|
|
43
|
+
export interface ParseLafsResponseOptions {
|
|
44
|
+
requireRegisteredErrorCode?: boolean;
|
|
45
|
+
}
|
|
46
|
+
export declare function parseLafsResponse<T = unknown>(input: unknown, options?: ParseLafsResponseOptions): T;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { isRegisteredErrorCode } 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
|
+
function normalizeError(error) {
|
|
26
|
+
return {
|
|
27
|
+
code: error.code,
|
|
28
|
+
message: error.message,
|
|
29
|
+
category: (error.category ?? "INTERNAL"),
|
|
30
|
+
retryable: error.retryable ?? false,
|
|
31
|
+
retryAfterMs: error.retryAfterMs ?? null,
|
|
32
|
+
details: error.details ?? {},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export function createEnvelope(input) {
|
|
36
|
+
const meta = createMeta(input.meta);
|
|
37
|
+
if (input.success) {
|
|
38
|
+
return {
|
|
39
|
+
$schema: LAFS_SCHEMA_URL,
|
|
40
|
+
_meta: meta,
|
|
41
|
+
success: true,
|
|
42
|
+
result: input.result,
|
|
43
|
+
...(input.page !== undefined ? { page: input.page } : {}),
|
|
44
|
+
...(input.error !== undefined ? { error: null } : {}),
|
|
45
|
+
...(input._extensions !== undefined ? { _extensions: input._extensions } : {}),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
$schema: LAFS_SCHEMA_URL,
|
|
50
|
+
_meta: meta,
|
|
51
|
+
success: false,
|
|
52
|
+
result: null,
|
|
53
|
+
error: normalizeError(input.error),
|
|
54
|
+
...(input.page !== undefined ? { page: input.page } : {}),
|
|
55
|
+
...(input._extensions !== undefined ? { _extensions: input._extensions } : {}),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
export class LafsError extends Error {
|
|
59
|
+
code;
|
|
60
|
+
category;
|
|
61
|
+
retryable;
|
|
62
|
+
retryAfterMs;
|
|
63
|
+
details;
|
|
64
|
+
registered;
|
|
65
|
+
constructor(error) {
|
|
66
|
+
super(error.message);
|
|
67
|
+
this.name = "LafsError";
|
|
68
|
+
this.code = error.code;
|
|
69
|
+
this.category = error.category;
|
|
70
|
+
this.retryable = error.retryable;
|
|
71
|
+
this.retryAfterMs = error.retryAfterMs;
|
|
72
|
+
this.details = error.details;
|
|
73
|
+
this.registered = isRegisteredErrorCode(error.code);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
export function parseLafsResponse(input, options = {}) {
|
|
77
|
+
const envelope = assertEnvelope(input);
|
|
78
|
+
if (envelope.success) {
|
|
79
|
+
return envelope.result;
|
|
80
|
+
}
|
|
81
|
+
const error = envelope.error;
|
|
82
|
+
if (!error) {
|
|
83
|
+
throw new Error("Invalid LAFS envelope: success=false requires error object");
|
|
84
|
+
}
|
|
85
|
+
if (options.requireRegisteredErrorCode && !isRegisteredErrorCode(error.code)) {
|
|
86
|
+
throw new Error(`Unregistered LAFS error code: ${error.code}`);
|
|
87
|
+
}
|
|
88
|
+
throw new LafsError(error);
|
|
89
|
+
}
|
|
@@ -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
|
+
}
|