@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
package/dist/src/cli.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* LAFS Conformance CLI — diagnostic/human-readable tool.
|
|
4
|
+
*
|
|
5
|
+
* This CLI is a **diagnostic utility** that validates envelopes and flags
|
|
6
|
+
* against the LAFS schema and conformance checks. It is NOT itself a
|
|
7
|
+
* LAFS-conformant envelope producer. Its output is for human consumption
|
|
8
|
+
* and CI pipelines, not for machine-to-machine chaining.
|
|
9
|
+
*
|
|
10
|
+
* Exemption: The CLI is exempt from LAFS envelope conformance requirements.
|
|
11
|
+
* Its output format is not a LAFS envelope and MUST NOT be validated as one.
|
|
12
|
+
*
|
|
13
|
+
* @task T042
|
|
14
|
+
* @epic T034
|
|
15
|
+
*/
|
|
16
|
+
import { readFile } from "node:fs/promises";
|
|
17
|
+
import { runEnvelopeConformance, runFlagConformance } from "./conformance.js";
|
|
18
|
+
function parseArgs(argv) {
|
|
19
|
+
const args = {};
|
|
20
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
21
|
+
const current = argv[i];
|
|
22
|
+
const next = argv[i + 1];
|
|
23
|
+
if (current === "--envelope" && next) {
|
|
24
|
+
args.envelopePath = next;
|
|
25
|
+
i += 1;
|
|
26
|
+
}
|
|
27
|
+
else if (current === "--flags" && next) {
|
|
28
|
+
args.flagsPath = next;
|
|
29
|
+
i += 1;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return args;
|
|
33
|
+
}
|
|
34
|
+
async function readJson(path) {
|
|
35
|
+
const content = await readFile(path, "utf8");
|
|
36
|
+
return JSON.parse(content);
|
|
37
|
+
}
|
|
38
|
+
async function main() {
|
|
39
|
+
const args = parseArgs(process.argv.slice(2));
|
|
40
|
+
const reports = [];
|
|
41
|
+
if (args.envelopePath) {
|
|
42
|
+
const envelope = await readJson(args.envelopePath);
|
|
43
|
+
reports.push({ name: "envelope", report: runEnvelopeConformance(envelope) });
|
|
44
|
+
}
|
|
45
|
+
if (args.flagsPath) {
|
|
46
|
+
const flags = await readJson(args.flagsPath);
|
|
47
|
+
reports.push({ name: "flags", report: runFlagConformance(flags) });
|
|
48
|
+
}
|
|
49
|
+
if (reports.length === 0) {
|
|
50
|
+
throw new Error("Provide --envelope and/or --flags JSON files.");
|
|
51
|
+
}
|
|
52
|
+
console.log(JSON.stringify({ success: true, reports }, null, 2));
|
|
53
|
+
}
|
|
54
|
+
main().catch((error) => {
|
|
55
|
+
console.error(JSON.stringify({
|
|
56
|
+
success: false,
|
|
57
|
+
error: {
|
|
58
|
+
code: "E_INTERNAL_UNEXPECTED",
|
|
59
|
+
message: error instanceof Error ? error.message : String(error)
|
|
60
|
+
}
|
|
61
|
+
}, null, 2));
|
|
62
|
+
process.exit(1);
|
|
63
|
+
});
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ConformanceReport, FlagInput } from "./types.js";
|
|
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;
|
|
7
|
+
export declare function runFlagConformance(flags: FlagInput): ConformanceReport;
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { getTransportMapping, isRegisteredErrorCode, getRegistryCode } from "./errorRegistry.js";
|
|
2
|
+
import { resolveOutputFormat, LAFSFlagError } from "./flagSemantics.js";
|
|
3
|
+
import { isAgentAction, isMVILevel } from "./types.js";
|
|
4
|
+
import { getChecksForTier } from "./conformanceProfiles.js";
|
|
5
|
+
import { validateEnvelope } from "./validateEnvelope.js";
|
|
6
|
+
function pushCheck(checks, name, pass, detail) {
|
|
7
|
+
checks.push({ name, pass, ...(detail ? { detail } : {}) });
|
|
8
|
+
}
|
|
9
|
+
export function runEnvelopeConformance(envelope, options = {}) {
|
|
10
|
+
const checks = [];
|
|
11
|
+
const validation = validateEnvelope(envelope);
|
|
12
|
+
pushCheck(checks, "envelope_schema_valid", validation.valid, validation.valid ? undefined : validation.errors.join("; "));
|
|
13
|
+
if (!validation.valid) {
|
|
14
|
+
return { ok: false, checks };
|
|
15
|
+
}
|
|
16
|
+
const typed = envelope;
|
|
17
|
+
// envelope_invariants: success=true allows error to be null OR omitted;
|
|
18
|
+
// success=false requires error to be a non-null object.
|
|
19
|
+
// result MAY be non-null on error — validation tools (linters, type checkers)
|
|
20
|
+
// need to return actionable data (e.g., suggestedFix) alongside the error.
|
|
21
|
+
const invariant = typed.success
|
|
22
|
+
? typed.error == null // null or undefined (omitted) both valid for success
|
|
23
|
+
: typed.error != null; // error must be present, result is optional
|
|
24
|
+
pushCheck(checks, "envelope_invariants", invariant, invariant
|
|
25
|
+
? undefined
|
|
26
|
+
: typed.success
|
|
27
|
+
? "success=true but error is present and non-null"
|
|
28
|
+
: "success=false requires error to be a non-null object");
|
|
29
|
+
// error_code_registered: only checked when error is present (error is optional when success=true)
|
|
30
|
+
if (typed.error) {
|
|
31
|
+
const registered = isRegisteredErrorCode(typed.error.code);
|
|
32
|
+
pushCheck(checks, "error_code_registered", registered, registered ? undefined : `unregistered code: ${typed.error.code}`);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
pushCheck(checks, "error_code_registered", true, "error field absent or null — skipped (optional when success=true)");
|
|
36
|
+
}
|
|
37
|
+
// agent_action_valid: if error.agentAction is present, it must be a valid LAFSAgentAction
|
|
38
|
+
if (typed.error && typed.error.agentAction !== undefined) {
|
|
39
|
+
const valid = isAgentAction(typed.error.agentAction);
|
|
40
|
+
pushCheck(checks, "agent_action_valid", valid, valid ? undefined : `invalid agentAction: ${String(typed.error.agentAction)}`);
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
pushCheck(checks, "agent_action_valid", true, "agentAction absent — skipped");
|
|
44
|
+
}
|
|
45
|
+
// error_registry_agent_action: if error code is registered and agentAction is present,
|
|
46
|
+
// check if it matches the registry default (warning-level, not a hard failure)
|
|
47
|
+
if (typed.error && typed.error.agentAction !== undefined && isRegisteredErrorCode(typed.error.code)) {
|
|
48
|
+
const registryEntry = getRegistryCode(typed.error.code);
|
|
49
|
+
const registryAction = registryEntry?.["agentAction"];
|
|
50
|
+
if (registryAction) {
|
|
51
|
+
const matches = typed.error.agentAction === registryAction;
|
|
52
|
+
pushCheck(checks, "error_registry_agent_action", true, // always passes — advisory only
|
|
53
|
+
matches
|
|
54
|
+
? undefined
|
|
55
|
+
: `agentAction "${typed.error.agentAction}" differs from registry default "${registryAction}" for ${typed.error.code} (advisory)`);
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
pushCheck(checks, "error_registry_agent_action", true, "registry entry has no default agentAction — skipped");
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
pushCheck(checks, "error_registry_agent_action", true, typed.error ? "agentAction absent or code unregistered — skipped" : "no error present — skipped");
|
|
63
|
+
}
|
|
64
|
+
// transport_mapping_consistent: when an error is present, ensure the code has
|
|
65
|
+
// a transport-specific mapping in the registry for the declared transport.
|
|
66
|
+
if (typed.error) {
|
|
67
|
+
if (typed._meta.transport === "sdk") {
|
|
68
|
+
pushCheck(checks, "transport_mapping_consistent", true, "sdk transport does not require external status-code mapping");
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
const mapping = getTransportMapping(typed.error.code, typed._meta.transport);
|
|
72
|
+
const mappingOk = mapping !== null;
|
|
73
|
+
pushCheck(checks, "transport_mapping_consistent", mappingOk, mappingOk
|
|
74
|
+
? undefined
|
|
75
|
+
: `no ${typed._meta.transport} mapping found for code ${typed.error.code}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
pushCheck(checks, "transport_mapping_consistent", true, "no error present — mapping check skipped");
|
|
80
|
+
}
|
|
81
|
+
// context_mutation_failure: if the producer marks context as required for a
|
|
82
|
+
// mutation operation, missing context must fail with a context error code.
|
|
83
|
+
{
|
|
84
|
+
const ext = (typed._extensions ?? {});
|
|
85
|
+
const contextObj = (ext["context"] ?? {});
|
|
86
|
+
const lafsObj = (ext["lafs"] ?? {});
|
|
87
|
+
const contextRequired = ext["lafsContextRequired"] === true ||
|
|
88
|
+
contextObj["required"] === true ||
|
|
89
|
+
lafsObj["contextRequired"] === true;
|
|
90
|
+
if (!contextRequired) {
|
|
91
|
+
pushCheck(checks, "context_mutation_failure", true, "context not marked required — skipped");
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
const hasContextIdentity = typed._meta.contextVersion > 0 || Boolean(typed._meta.sessionId);
|
|
95
|
+
if (typed.success) {
|
|
96
|
+
const pass = hasContextIdentity;
|
|
97
|
+
pushCheck(checks, "context_mutation_failure", pass, pass ? undefined : "context required but missing identity (expect E_CONTEXT_MISSING)");
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
const code = typed.error?.code;
|
|
101
|
+
const pass = code === "E_CONTEXT_MISSING" || code === "E_CONTEXT_STALE";
|
|
102
|
+
pushCheck(checks, "context_mutation_failure", pass, pass
|
|
103
|
+
? undefined
|
|
104
|
+
: `context required failures should return E_CONTEXT_MISSING or E_CONTEXT_STALE, got ${String(code)}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const mviValid = isMVILevel(typed._meta.mvi);
|
|
109
|
+
pushCheck(checks, "meta_mvi_present", mviValid, mviValid ? undefined : `invalid mvi level: ${String(typed._meta.mvi)}`);
|
|
110
|
+
pushCheck(checks, "meta_strict_present", typeof typed._meta.strict === "boolean");
|
|
111
|
+
// strict_mode_behavior: when strict=true, the envelope MUST NOT contain
|
|
112
|
+
// explicit null for optional fields that can be omitted (page, error on success).
|
|
113
|
+
if (typed._meta.strict) {
|
|
114
|
+
const obj = envelope;
|
|
115
|
+
const hasExplicitNullError = typed.success && "error" in obj && obj["error"] === null;
|
|
116
|
+
const hasExplicitNullPage = "page" in obj && obj["page"] === null;
|
|
117
|
+
const strictClean = !hasExplicitNullError && !hasExplicitNullPage;
|
|
118
|
+
pushCheck(checks, "strict_mode_behavior", strictClean, strictClean
|
|
119
|
+
? undefined
|
|
120
|
+
: "strict mode: optional fields should be omitted rather than set to null");
|
|
121
|
+
}
|
|
122
|
+
// pagination_mode_consistent: when page is present and is an object, verify
|
|
123
|
+
// that the fields present match the declared pagination mode.
|
|
124
|
+
if (typed.page && typeof typed.page === "object") {
|
|
125
|
+
const page = typed.page;
|
|
126
|
+
const mode = page["mode"];
|
|
127
|
+
let consistent = true;
|
|
128
|
+
let detail;
|
|
129
|
+
if (mode === "cursor") {
|
|
130
|
+
if (page["offset"] !== undefined) {
|
|
131
|
+
consistent = false;
|
|
132
|
+
detail = "cursor mode should not include offset field";
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
else if (mode === "offset") {
|
|
136
|
+
if (page["nextCursor"] !== undefined) {
|
|
137
|
+
consistent = false;
|
|
138
|
+
detail = "offset mode should not include nextCursor field";
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
else if (mode === "none") {
|
|
142
|
+
const extraFields = Object.keys(page).filter((k) => k !== "mode");
|
|
143
|
+
if (extraFields.length > 0) {
|
|
144
|
+
consistent = false;
|
|
145
|
+
detail = `none mode should only have mode field, found: ${extraFields.join(", ")}`;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
pushCheck(checks, "pagination_mode_consistent", consistent, consistent ? undefined : detail);
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
pushCheck(checks, "pagination_mode_consistent", true, "page absent — skipped");
|
|
152
|
+
}
|
|
153
|
+
// strict_mode_enforced: verify the schema enforces additional-property rules.
|
|
154
|
+
// When strict=true, extra top-level properties must be rejected by validation.
|
|
155
|
+
// When strict=false, extra top-level properties must be allowed.
|
|
156
|
+
{
|
|
157
|
+
const extraPropEnvelope = { ...envelope, _unknown_extra: true };
|
|
158
|
+
const extraResult = validateEnvelope(extraPropEnvelope);
|
|
159
|
+
if (typed._meta.strict) {
|
|
160
|
+
pushCheck(checks, "strict_mode_enforced", !extraResult.valid, extraResult.valid ? "strict=true but additional properties were accepted" : undefined);
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
pushCheck(checks, "strict_mode_enforced", extraResult.valid, !extraResult.valid ? "strict=false but additional properties were rejected" : undefined);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// context_preservation_valid: validate monotonic context version behavior and
|
|
167
|
+
// context-constraint integrity when a context ledger extension is present.
|
|
168
|
+
{
|
|
169
|
+
const ext = (typed._extensions ?? {});
|
|
170
|
+
const ledger = (ext["contextLedger"] ?? ext["context"]);
|
|
171
|
+
if (!ledger || typeof ledger !== "object") {
|
|
172
|
+
pushCheck(checks, "context_preservation_valid", true, "context ledger absent — skipped");
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
const version = ledger["version"];
|
|
176
|
+
const previousVersion = ledger["previousVersion"];
|
|
177
|
+
const removedConstraints = ledger["removedConstraints"];
|
|
178
|
+
const hasNumericVersion = typeof version === "number";
|
|
179
|
+
const matchesEnvelopeVersion = hasNumericVersion && version === typed._meta.contextVersion;
|
|
180
|
+
const monotonicFromPrevious = typeof previousVersion !== "number" || (hasNumericVersion && version >= previousVersion);
|
|
181
|
+
const constraintsPreserved = !Array.isArray(removedConstraints) || removedConstraints.length === 0 || !typed.success;
|
|
182
|
+
let pass = matchesEnvelopeVersion && monotonicFromPrevious && constraintsPreserved;
|
|
183
|
+
let detail;
|
|
184
|
+
if (!hasNumericVersion) {
|
|
185
|
+
pass = false;
|
|
186
|
+
detail = "context ledger version must be numeric";
|
|
187
|
+
}
|
|
188
|
+
else if (!matchesEnvelopeVersion) {
|
|
189
|
+
detail = `context version mismatch: ledger=${String(version)} envelope=${typed._meta.contextVersion}`;
|
|
190
|
+
}
|
|
191
|
+
else if (!monotonicFromPrevious) {
|
|
192
|
+
detail = `non-monotonic context version: previous=${String(previousVersion)} current=${String(version)}`;
|
|
193
|
+
}
|
|
194
|
+
else if (!constraintsPreserved) {
|
|
195
|
+
detail = "context constraint removal detected on successful response";
|
|
196
|
+
}
|
|
197
|
+
// Error-path validation for stale/missing context signaling.
|
|
198
|
+
if (!typed.success && typed.error && ledger["required"] === true) {
|
|
199
|
+
const stale = ledger["stale"] === true;
|
|
200
|
+
if (stale && typed.error.code !== "E_CONTEXT_STALE") {
|
|
201
|
+
pass = false;
|
|
202
|
+
detail = `stale context should return E_CONTEXT_STALE, got ${typed.error.code}`;
|
|
203
|
+
}
|
|
204
|
+
if (!stale && typed.error.code !== "E_CONTEXT_MISSING" && typed.error.code !== "E_CONTEXT_STALE") {
|
|
205
|
+
pass = false;
|
|
206
|
+
detail = `required context failure should return E_CONTEXT_MISSING or E_CONTEXT_STALE, got ${typed.error.code}`;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
pushCheck(checks, "context_preservation_valid", pass, detail);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
const tier = options.tier;
|
|
213
|
+
if (!tier) {
|
|
214
|
+
return { ok: checks.every((check) => check.pass), checks };
|
|
215
|
+
}
|
|
216
|
+
const allowed = new Set(getChecksForTier(tier));
|
|
217
|
+
const tierChecks = checks.filter((check) => allowed.has(check.name));
|
|
218
|
+
return { ok: tierChecks.every((check) => check.pass), checks: tierChecks };
|
|
219
|
+
}
|
|
220
|
+
export function runFlagConformance(flags) {
|
|
221
|
+
const checks = [];
|
|
222
|
+
try {
|
|
223
|
+
const resolved = resolveOutputFormat(flags);
|
|
224
|
+
pushCheck(checks, "flag_conflict_rejected", !(flags.humanFlag && flags.jsonFlag));
|
|
225
|
+
// Protocol-default check: when nothing is specified (source === "default"),
|
|
226
|
+
// the protocol requires JSON as the default format.
|
|
227
|
+
const isProtocolDefault = resolved.source === "default";
|
|
228
|
+
pushCheck(checks, "json_protocol_default", !isProtocolDefault || resolved.format === "json", isProtocolDefault && resolved.format !== "json"
|
|
229
|
+
? `protocol default should be json, got ${resolved.format}`
|
|
230
|
+
: undefined);
|
|
231
|
+
// Config-override check: when a project or user default is active,
|
|
232
|
+
// the resolved format must match the config-provided value.
|
|
233
|
+
const hasConfigOverride = resolved.source === "project" || resolved.source === "user";
|
|
234
|
+
const expectedOverride = resolved.source === "project" ? flags.projectDefault : flags.userDefault;
|
|
235
|
+
pushCheck(checks, "config_override_respected", !hasConfigOverride || resolved.format === expectedOverride, hasConfigOverride && resolved.format !== expectedOverride
|
|
236
|
+
? `config override expected ${String(expectedOverride)}, got ${resolved.format}`
|
|
237
|
+
: undefined);
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
if (error instanceof LAFSFlagError && error.code === "E_FORMAT_CONFLICT") {
|
|
241
|
+
pushCheck(checks, "flag_conflict_rejected", true);
|
|
242
|
+
return { ok: checks.every((check) => check.pass), checks };
|
|
243
|
+
}
|
|
244
|
+
pushCheck(checks, "flag_resolution", false, error instanceof Error ? error.message : String(error));
|
|
245
|
+
return { ok: false, checks };
|
|
246
|
+
}
|
|
247
|
+
return { ok: checks.every((check) => check.pass), checks };
|
|
248
|
+
}
|
|
@@ -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
|
+
}
|