Skip to content

Commit 2c40718

Browse files
feat(sdk): add field extraction utilities for _fields and _mvi (§9.1, §9.2)
Implements SDK runtime utilities that were missing per GitHub Issue #1: resolveFieldExtraction(), extractFieldFromResult/Envelope(), applyFieldFilter(), isMVILevel() type guard, MVI_LEVELS constant, and E_FIELD_CONFLICT error code. Enhances LAFSFlagError to implement LAFSError interface. Amends spec §9.1/§9.2 with MVI level behavioral definitions and wrapper-pattern field selection. Closes #1 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent f41ac6a commit 2c40718

15 files changed

Lines changed: 740 additions & 19 deletions

CHANGELOG.md

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,37 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [1.5.0] - 2026-02-26
11+
12+
### Added
13+
14+
- **Field extraction utilities** (`src/fieldExtraction.ts`): runtime SDK support for spec-defined `_fields` (§9.2) and `_mvi` (§9.1) features:
15+
- `resolveFieldExtraction()` — resolves `--field`, `--fields`, and `--mvi` CLI flags with conflict detection
16+
- `extractFieldFromResult()` / `extractFieldFromEnvelope()` — single-field extraction across four result shapes (flat, wrapper-entity, wrapper-array, direct array)
17+
- `applyFieldFilter()` — multi-field projection that preserves envelope structure and sets `_meta.mvi = 'custom'` per §9.1
18+
- `FieldExtractionInput`, `FieldExtractionResolution` — supporting types
19+
- **`MVI_LEVELS` constant** (`src/types.ts`): `ReadonlySet<MVILevel>` for CLI completion generators, validation schemas, and docs tools
20+
- **`isMVILevel()` type guard** (`src/types.ts`): runtime type narrowing for `MVILevel` values
21+
- **`E_FIELD_CONFLICT` error code** (`schemas/v1/error-registry.json`): for mutually exclusive `--field` + `--fields` flag combinations (category: `CONTRACT`, HTTP 400, gRPC `INVALID_ARGUMENT`, CLI exit 2)
22+
- **Test fixtures**: `fixtures/field-extraction-success.json` (flat result) and `fixtures/field-extraction-array.json` (direct array result)
23+
- **44 new tests** in `tests/fieldExtraction.test.ts` covering all functions, edge cases, and integration flow
24+
- **Migration note** (`migrations/1.4.1-to-1.5.0.md`): documents `_meta` always-present clarification and new exports
25+
26+
### Changed
27+
28+
- **`LAFSFlagError`** (`src/flagSemantics.ts`): now implements `LAFSError` interface with `category`, `retryable`, `retryAfterMs`, and `details` properties resolved from the error registry. Constructor accepts optional third `details` parameter (backwards compatible)
29+
- **Conformance MVI check** (`src/conformance.ts`): replaced hardcoded `validMviLevels` array with `isMVILevel()` type guard (check name `meta_mvi_present` unchanged)
30+
31+
### Specification
32+
33+
- **§9.1 MVI default** (`lafs.md`): added behavioral definitions for each MVI level (`minimal`, `standard`, `full`, `custom`), clarified that `_meta` is a structural envelope field that MUST always be present regardless of MVI level, and that `custom` is server-set only (not client-requestable)
34+
- **§9.2 Field selection** (`lafs.md`): expanded to document wrapper-entity and wrapper-array result shapes, path notation exclusion, array-element projection, and automatic `_meta.mvi = 'custom'` when `_fields` is present
35+
36+
### Statistics
37+
38+
- 337 total tests passing (44 new)
39+
- 13 error codes in registry (1 new)
40+
1041
## [1.4.1] - 2026-02-25
1142

1243
### Added
@@ -451,8 +482,18 @@ This is a major release (1.0.0) marking production readiness. All previously dep
451482

452483
---
453484

454-
[Unreleased]: https://github.com/lafs-protocol/lafs-protocol/compare/v1.0.0...HEAD
455-
[1.0.0]: https://github.com/lafs-protocol/lafs-protocol/releases/tag/v1.0.0
485+
[Unreleased]: https://github.com/kryptobaseddev/lafs-protocol/compare/v1.5.0...HEAD
486+
[1.5.0]: https://github.com/kryptobaseddev/lafs-protocol/compare/v1.4.1...v1.5.0
487+
[1.4.1]: https://github.com/kryptobaseddev/lafs-protocol/compare/v1.4.0...v1.4.1
488+
[1.4.0]: https://github.com/kryptobaseddev/lafs-protocol/compare/v1.3.2...v1.4.0
489+
[1.3.2]: https://github.com/kryptobaseddev/lafs-protocol/compare/v1.3.1...v1.3.2
490+
[1.3.1]: https://github.com/kryptobaseddev/lafs-protocol/compare/v1.3.0...v1.3.1
491+
[1.3.0]: https://github.com/kryptobaseddev/lafs-protocol/compare/v1.2.3...v1.3.0
492+
[1.2.3]: https://github.com/kryptobaseddev/lafs-protocol/compare/v1.2.2...v1.2.3
493+
[1.2.2]: https://github.com/kryptobaseddev/lafs-protocol/compare/v1.2.0...v1.2.2
494+
[1.2.0]: https://github.com/kryptobaseddev/lafs-protocol/compare/v1.1.0...v1.2.0
495+
[1.1.0]: https://github.com/kryptobaseddev/lafs-protocol/compare/v1.0.0...v1.1.0
496+
[1.0.0]: https://github.com/kryptobaseddev/lafs-protocol/releases/tag/v1.0.0
456497
[0.5.0]: https://github.com/lafs-protocol/lafs-protocol/releases/tag/v0.5.0
457498
[0.4.0]: https://github.com/lafs-protocol/lafs-protocol/releases/tag/v0.4.0
458499
[0.3.0]: https://github.com/lafs-protocol/lafs-protocol/releases/tag/v0.3.0

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
LAFS defines a standard envelope format for structured responses from LLM-powered agents and tools. It complements transport protocols like [MCP](https://modelcontextprotocol.io/) and [A2A](https://github.com/google/A2A) by standardizing what comes back — not how it gets there.
66

7-
**Current version:** 1.4.1 | [📚 Documentation](https://codluv.gitbook.io/lafs-protocol/) | [Spec](lafs.md) | [Migration Guides](migrations/)
7+
**Current version:** 1.5.0 | [📚 Documentation](https://codluv.gitbook.io/lafs-protocol/) | [Spec](lafs.md) | [Migration Guides](migrations/)
88

99
[![GitBook](https://img.shields.io/badge/docs-gitbook-blue)](https://codluv.gitbook.io/lafs-protocol/)
1010
[![npm](https://img.shields.io/npm/v/@cleocode/lafs-protocol)](https://www.npmjs.com/package/@cleocode/lafs-protocol)

docs/specification.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# LAFS: LLM-Agent-First Specification
22

33
> 📚 **Documentation:** https://codluv.gitbook.io/lafs-protocol/
4-
> **Version:** 1.4.1 | **Status:** Production Ready
4+
> **Version:** 1.5.0 | **Status:** Production Ready
55
66
## 1. Scope
77

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"$schema": "https://lafs.dev/schemas/v1/envelope.schema.json",
3+
"_meta": {
4+
"specVersion": "1.0.0",
5+
"schemaVersion": "1.0.0",
6+
"timestamp": "2026-02-26T00:00:00Z",
7+
"operation": "tasks.list",
8+
"requestId": "req_field_02",
9+
"transport": "sdk",
10+
"strict": true,
11+
"mvi": "standard",
12+
"contextVersion": 0
13+
},
14+
"success": true,
15+
"result": [
16+
{ "id": "T001", "title": "Task One", "status": "active" },
17+
{ "id": "T002", "title": "Task Two", "status": "pending" }
18+
]
19+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"$schema": "https://lafs.dev/schemas/v1/envelope.schema.json",
3+
"_meta": {
4+
"specVersion": "1.0.0",
5+
"schemaVersion": "1.0.0",
6+
"timestamp": "2026-02-26T00:00:00Z",
7+
"operation": "tasks.show",
8+
"requestId": "req_field_01",
9+
"transport": "sdk",
10+
"strict": true,
11+
"mvi": "standard",
12+
"contextVersion": 0
13+
},
14+
"success": true,
15+
"result": {
16+
"id": "T001",
17+
"title": "Example Task",
18+
"status": "active",
19+
"description": "A verbose description",
20+
"tags": ["a", "b"]
21+
}
22+
}

lafs.md

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# LAFS: LLM-Agent-First Specification
22

33
> 📚 **Documentation:** https://codluv.gitbook.io/lafs-protocol/
4-
> **Version:** 1.4.1 | **Status:** Production Ready
4+
> **Version:** 1.5.0 | **Status:** Production Ready
55
66
## 1. Scope
77

@@ -431,16 +431,45 @@ To reduce token and I/O overhead, implementations SHOULD support lazy retrieval
431431
- Default list/batch outputs MUST only contain fields required for next action.
432432
- Verbose fields SHOULD be omitted by default.
433433
- Systems SHOULD publish operation-level MVI budgets.
434+
- `_meta.mvi` MUST be one of: `minimal`, `standard`, `full`, or `custom`.
435+
- `_meta` is a structural envelope field and MUST always be present regardless
436+
of `mvi` level. MVI levels govern the contents of `result` only; they MUST NOT
437+
affect envelope structural fields (`$schema`, `_meta`, `success`, `error`,
438+
`page`, `_extensions`).
439+
- `minimal`: MUST include only fields within `result` sufficient for the next
440+
agent action (typically identifiers and status). Implementations SHOULD
441+
document which fields constitute `minimal` per operation.
442+
- `standard` (default): MUST include all commonly useful fields for the
443+
operation.
444+
- `full`: MUST include all available fields including verbose and
445+
rarely-accessed data.
446+
- `custom`: MUST be set by the server when `_fields` projection has been
447+
applied, indicating the result does not conform to any predefined disclosure
448+
level. `custom` is not a client-requestable level.
434449

435450
### 9.2 Field selection (`_fields`)
436451

437-
Clients MAY request a subset of response fields via the `_fields` request parameter.
438-
439-
- `_fields` MUST be an array of strings identifying top-level result field names.
440-
- When `_fields` is present, the server MUST return only the requested fields plus any MVI-required fields for the declared disclosure level.
441-
- When `_fields` is absent, the server MUST return fields appropriate for the declared `_meta.mvi` disclosure level.
442-
- If a requested field does not exist on the resource, the server SHOULD omit it silently (no error). Servers MAY include a warning in `_meta.warnings` for unknown fields.
443-
- `_fields` MUST NOT affect envelope structural fields (`$schema`, `_meta`, `success`, `error`, `page`, `_extensions`); it applies only to the contents of `result`.
452+
Clients MAY request a subset of response fields via the `_fields` request
453+
parameter.
454+
455+
- `_fields` MUST be an array of strings identifying `result` field names.
456+
Path notation (e.g., `task.title`) is not defined by this specification.
457+
- When `result` is an array, `_fields` applies to the keys of each element.
458+
- When `result` is a wrapper object whose values are entities or arrays of
459+
entities (e.g., `{ "task": { ... } }` or `{ "items": [...] }`), servers
460+
SHOULD apply `_fields` to the nested entity fields rather than the wrapper's
461+
own keys.
462+
- When `_fields` is present, the server MUST return only the requested fields
463+
plus any MVI-required fields for the declared disclosure level.
464+
The server MUST set `_meta.mvi` to `custom` in the response.
465+
- When `_fields` is absent, the server MUST return fields appropriate for the
466+
declared `_meta.mvi` disclosure level.
467+
- If a requested field does not exist on the resource, the server SHOULD omit
468+
it silently (no error). Servers MAY include a warning in `_meta.warnings`
469+
for unknown fields.
470+
- `_fields` MUST NOT affect envelope structural fields (`$schema`, `_meta`,
471+
`success`, `error`, `page`, `_extensions`); it applies only to the contents
472+
of `result`.
444473

445474
### 9.3 Expansion mechanism (`_expand`)
446475

migrations/1.4.1-to-1.5.0.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Migration: 1.4.1 → 1.5.0
2+
3+
## `_meta` is always required (§9.1 clarification)
4+
5+
The §9.1 amendment clarifies that `_meta` is a structural envelope field and
6+
MUST be present regardless of `mvi` level. Existing implementations that strip
7+
`_meta` for `mvi: 'minimal'` must be updated to always include `_meta`. MVI
8+
levels govern `result` contents only.
9+
10+
## New exports
11+
12+
- `MVI_LEVELS``ReadonlySet<MVILevel>` of all valid MVI levels
13+
- `isMVILevel()` — type guard for `MVILevel`
14+
- `resolveFieldExtraction()` — resolves `--field`, `--fields`, and `--mvi` flags
15+
- `extractFieldFromResult()` / `extractFieldFromEnvelope()` — single-field extraction
16+
- `applyFieldFilter()` — multi-field projection with `_meta.mvi = 'custom'`
17+
- `FieldExtractionInput`, `FieldExtractionResolution` — supporting types
18+
19+
## Enhanced `LAFSFlagError`
20+
21+
`LAFSFlagError` now implements `LAFSError`, adding `category`, `retryable`,
22+
`retryAfterMs`, and `details` properties resolved from the error registry.
23+
The constructor accepts an optional third `details` parameter. This is a
24+
backwards-compatible addition.
25+
26+
## New error code: `E_FIELD_CONFLICT`
27+
28+
Added to the error registry for mutually exclusive field selection modes
29+
(`--field` + `--fields`). Category: `CONTRACT`, HTTP 400, gRPC
30+
`INVALID_ARGUMENT`, CLI exit 2.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@cleocode/lafs-protocol",
3-
"version": "1.4.1",
3+
"version": "1.5.0",
44
"private": false,
55
"type": "module",
66
"description": "LLM-Agent-First Specification schemas and conformance tooling",

schemas/v1/error-registry.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,15 @@
109109
"httpStatus": 413,
110110
"grpcStatus": "RESOURCE_EXHAUSTED",
111111
"cliExit": 2
112+
},
113+
{
114+
"code": "E_FIELD_CONFLICT",
115+
"category": "CONTRACT",
116+
"description": "Mutually exclusive field selection modes (single-field extraction and multi-field filtering cannot be combined)",
117+
"retryable": false,
118+
"httpStatus": 400,
119+
"grpcStatus": "INVALID_ARGUMENT",
120+
"cliExit": 2
112121
}
113122
]
114123
}

src/conformance.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { getTransportMapping, isRegisteredErrorCode } from "./errorRegistry.js";
22
import { resolveOutputFormat, LAFSFlagError } from "./flagSemantics.js";
3+
import { isMVILevel } from "./types.js";
34
import type { ConformanceReport, FlagInput } from "./types.js";
45
import { getChecksForTier, type ConformanceTier } from "./conformanceProfiles.js";
56
import { validateEnvelope } from "./validateEnvelope.js";
@@ -161,12 +162,12 @@ export function runEnvelopeConformance(
161162
}
162163
}
163164

164-
const validMviLevels = ["minimal", "standard", "full", "custom"];
165+
const mviValid = isMVILevel(typed._meta.mvi);
165166
pushCheck(
166167
checks,
167168
"meta_mvi_present",
168-
validMviLevels.includes(typed._meta.mvi),
169-
validMviLevels.includes(typed._meta.mvi) ? undefined : `invalid mvi level: ${String(typed._meta.mvi)}`,
169+
mviValid,
170+
mviValid ? undefined : `invalid mvi level: ${String(typed._meta.mvi)}`,
170171
);
171172
pushCheck(checks, "meta_strict_present", typeof typed._meta.strict === "boolean");
172173

0 commit comments

Comments
 (0)