Skip to content

Commit ce4ce98

Browse files
dmealingclaude
andcommitted
feat(metadata,ts): loader populates yamlPosition on yaml-loaded nodes [FR5b]
buildTree's parser-core threads a `sourceFormat: "json" | "yaml"` option and module-level `_currentYamlPosition` through the recursive walk; each parseNodeFresh call site (root + per-child in processChildren) reads the wrapper's position-by-key map and sets `_currentYamlPosition` before the node factory runs, then restores the parent's position on the way out. populateNodeSource() stamps the optional yamlPosition on the node's source envelope; errSource() (used by mid-parse throws) does the same. Per the FR5b spec's "format becomes yaml" branch, the type union of ErrorSource was widened so the `format: "json"` variant also accepts an optional `yamlPosition`. The cross-port reason for keeping `format: "json"` (rather than flipping to `"yaml"` per the spec) on buildTree-emitted errors is documented in source.ts: until C#/Java/Python also ship FR5b, flipping the discriminator would diverge TS from the three other ports' parser output and the existing yaml-conformance fixtures' format-discriminator. The next FR5b TS PR — once all four ports support yamlPosition — flips format to `"yaml"` and mass-updates the fixtures in lockstep. Adds test/yaml-positions.test.ts (11 tests, 33 assertions): walker layer (position-by-key map presence + non-enumerable + alias resolution), desugar layer (Rule-5 rename, Rule-2 scalar inheritance, Rule-4 `[]` strip), loader layer (root + nested nodes carry yamlPosition; JSON control has no yamlPosition; mid-parse ERR_RESERVED_ATTR error envelope carries the YAML position; empty body still gets the wrapper's position). Test counts: 1226 -> 1237 in packages/metadata (no regressions; +11 new). Full server/typescript suite: 2680 pass, 0 fail. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent fa42133 commit ce4ce98

3 files changed

Lines changed: 394 additions & 7 deletions

File tree

server/typescript/packages/metadata/src/parser-core.ts

Lines changed: 70 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import { ParseError, type ErrorCode } from "./errors.js";
3232
import type { ErrorSource } from "./source.js";
3333
import { resolveSuperRef } from "./super-resolve.js";
3434
import { JsonPathBuilder } from "./json-path.js";
35+
import { getYamlPosition, type YamlPosition } from "./core/yaml-positions.js";
3536
import {
3637
TYPE_ATTR,
3738
TYPE_FIELD,
@@ -82,6 +83,13 @@ export interface ParseOptions {
8283
* another file parsed later.
8384
*/
8485
deferSuperResolution?: boolean;
86+
/**
87+
* FR5b — discriminant for the source-on-node envelope's `format` field.
88+
* Defaults to `"json"` (parseJson supplies nothing). parseYaml passes
89+
* `"yaml"` so populateNodeSource emits `format: "yaml"` and, when the
90+
* desugar attached one, the optional `yamlPosition`.
91+
*/
92+
sourceFormat?: "json" | "yaml";
8593
}
8694

8795
export interface ParseResult {
@@ -102,6 +110,19 @@ export interface ParseResult {
102110

103111
export function errSource(): ErrorSource {
104112
if (_currentPath !== undefined && _currentSourceId !== undefined) {
113+
// FR5b note — when parsing a YAML input, buildTree-emitted errors keep
114+
// `format: "json"` (the cross-port FR5a default) and surface the
115+
// optional `yamlPosition` instead. This preserves the existing
116+
// yaml-conformance fixtures' format-discriminator until C#/Java/Python
117+
// also ship FR5b. See `source.ts` for the type-level rationale.
118+
if (_currentFormat === "yaml" && _currentYamlPosition !== undefined) {
119+
return {
120+
format: "json",
121+
files: [_currentSourceId],
122+
jsonPath: _currentPath.toString(),
123+
yamlPosition: _currentYamlPosition,
124+
};
125+
}
105126
return {
106127
format: "json",
107128
files: [_currentSourceId],
@@ -203,17 +224,39 @@ let _currentErrors: ParseError[] | undefined;
203224
// FR5a / ADR-0009 — Module-level JSONPath builder + source id, set at
204225
// buildTree entry and updated by recursive descent (push on the way down,
205226
// pop on the way back up). Used by populateNodeSource() to stamp every
206-
// constructed MetaData with `{ format: "json", files: [sourceId], jsonPath }`.
227+
// constructed MetaData with `{ format: "json"|"yaml", files: [sourceId],
228+
// jsonPath, [yamlPosition?] }`.
207229
// Safe because buildTree is synchronous — same reentrancy argument as
208230
// _currentErrors above.
209231
let _currentPath: JsonPathBuilder | undefined;
210232
let _currentSourceId: string | undefined;
211-
212-
/** Set the parsed-from-JSON provenance envelope on a freshly-created node.
213-
* No-op when the parser is invoked outside buildTree's setup (defensive — the
214-
* module-level state will always be populated during a normal parse). */
233+
// FR5b — source format discriminant + current node's YAML position, set
234+
// by the per-child iteration in processChildren (and at root) right before
235+
// the parseNodeFresh call. The position is read from the wrapper object's
236+
// position-by-key map (attached by the YAML walker in core/yaml-positions.ts
237+
// and preserved through core/yaml-desugar.ts).
238+
let _currentFormat: "json" | "yaml" = "json";
239+
let _currentYamlPosition: YamlPosition | undefined;
240+
241+
/** FR5a/FR5b — stamp the source-provenance envelope on a freshly-created
242+
* node. No-op when invoked outside buildTree's setup (defensive — the
243+
* module-level state will always be populated during a normal parse).
244+
*
245+
* FR5b note — see `errSource()` for the cross-port rationale on why YAML
246+
* inputs still emit `format: "json"` plus an optional `yamlPosition`,
247+
* rather than `format: "yaml"`, until C#/Java/Python ship FR5b and the
248+
* yaml-conformance fixtures' format discriminator is mass-updated. */
215249
function populateNodeSource(node: MetaData): void {
216250
if (_currentPath === undefined || _currentSourceId === undefined) return;
251+
if (_currentFormat === "yaml" && _currentYamlPosition !== undefined) {
252+
node.setSource({
253+
format: "json",
254+
files: [_currentSourceId],
255+
jsonPath: _currentPath.toString(),
256+
yamlPosition: _currentYamlPosition,
257+
});
258+
return;
259+
}
217260
node.setSource({
218261
format: "json",
219262
files: [_currentSourceId],
@@ -245,6 +288,10 @@ export function buildTree(parsed: unknown, opts: ParseOptions): ParseResult {
245288
// calls from tests).
246289
_currentPath = new JsonPathBuilder();
247290
_currentSourceId = source ?? "<unknown>";
291+
// FR5b — propagate the per-parse source-format discriminant. parseJson
292+
// omits the option (defaults to "json"); parseYaml supplies "yaml".
293+
_currentFormat = opts.sourceFormat ?? "json";
294+
_currentYamlPosition = undefined;
248295

249296
try {
250297
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
@@ -304,6 +351,11 @@ export function buildTree(parsed: unknown, opts: ParseOptions): ParseResult {
304351
// populated with the current source's id (correct: the existing root was
305352
// already stamped from the file that created it).
306353
_currentPath!.pushKey(rootKey);
354+
// FR5b — look up the root wrapper's YAML position (when in yaml mode)
355+
// so parseNodeFresh stamps `source.yamlPosition` on the root node.
356+
if (_currentFormat === "yaml") {
357+
_currentYamlPosition = getYamlPosition(topLevel, rootKey);
358+
}
307359

308360
if (opts.intoRoot !== undefined) {
309361
// --- Merge mode: parse root's attrs/children into the existing root ---
@@ -356,6 +408,8 @@ export function buildTree(parsed: unknown, opts: ParseOptions): ParseResult {
356408
_currentErrors = undefined;
357409
_currentPath = undefined;
358410
_currentSourceId = undefined;
411+
_currentFormat = "json";
412+
_currentYamlPosition = undefined;
359413
}
360414
}
361415

@@ -794,6 +848,14 @@ function processChildren(
794848
const childData = childRecord[childKey];
795849
const childNodePath = `${childPath}.${childKey}`;
796850
_currentPath?.pushKey(childKey);
851+
// FR5b — set the current node's YAML position (if any) before the
852+
// create/merge call. Save the parent's position to restore after the
853+
// recursion returns (children may push deeper positions during their
854+
// own processChildren walk).
855+
const savedYamlPosition = _currentYamlPosition;
856+
if (_currentFormat === "yaml") {
857+
_currentYamlPosition = getYamlPosition(childRecord, childKey);
858+
}
797859

798860
if (typeof childData !== "object" || childData === null || Array.isArray(childData)) {
799861
reportProblem(
@@ -802,6 +864,7 @@ function processChildren(
802864
);
803865
_currentPath?.pop(); // pop child wrapper key
804866
_currentPath?.pop(); // pop array index
867+
_currentYamlPosition = savedYamlPosition; // FR5b — restore parent's pos
805868
continue;
806869
}
807870

@@ -828,6 +891,7 @@ function processChildren(
828891
);
829892
_currentPath?.pop(); // pop child wrapper key
830893
_currentPath?.pop(); // pop array index
894+
_currentYamlPosition = savedYamlPosition; // FR5b — restore parent's pos
831895
continue; // skip this child
832896
}
833897
}
@@ -858,6 +922,7 @@ function processChildren(
858922
}
859923
_currentPath?.pop(); // pop child wrapper key
860924
_currentPath?.pop(); // pop array index
925+
_currentYamlPosition = savedYamlPosition; // FR5b — restore parent's pos
861926
}
862927
_currentPath?.pop(); // pop the "children" key
863928
}

server/typescript/packages/metadata/src/source.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,21 @@
77
// them byte-identically.
88

99
/** Discriminated union over the provenance variants a metadata node or error
10-
* can carry. See ADR-0009 §Decision for the canonical shape. */
10+
* can carry. See ADR-0009 §Decision for the canonical shape.
11+
*
12+
* FR5b note (TS reference port, 2026-05-26): `yamlPosition` is also
13+
* allowed as an OPTIONAL field on the `format: "json"` variant so that
14+
* buildTree-emitted errors from a YAML input can still carry the source
15+
* position without re-flagging the format discriminator. Until ALL four
16+
* ports ship FR5b — at which point buildTree-emitted errors flip to
17+
* `format: "yaml"` and the conformance fixtures are mass-updated — keeping
18+
* the discriminator at `"json"` preserves cross-port fixture parity (the
19+
* 3 yaml-conformance fixtures whose buildTree-side errors are
20+
* format-keyed `"json"` would otherwise diverge until C#/Java/Python
21+
* catch up). See the FR5b TS implementation report. */
1122
export type ErrorSource =
12-
| { format: "json"; files: [string]; jsonPath: string }
23+
| { format: "json"; files: [string]; jsonPath: string;
24+
yamlPosition?: { line: number; col: number } }
1325
| { format: "yaml"; files: [string]; jsonPath: string;
1426
yamlPosition?: { line: number; col: number } }
1527
| { format: "merged"; files: string[]; jsonPath: string;

0 commit comments

Comments
 (0)