* Move every vendor-tagged field out of the shared `types` and into a `{ : { extends: * { : { via, fields } } } }` structure. A field's `via` names the spec extension point * it flowed through; the field having resolved into a real shared record (via the `//=` fold and * gr
(types)
| 698 | * @returns {object} The vendor section, empty when there are no vendor fields. |
| 699 | */ |
| 700 | function extractVendor(types) { |
| 701 | const vendor = {} |
| 702 | const anchors = new Set() |
| 703 | for (const [typeName, node] of Object.entries(types)) { |
| 704 | if (node.kind !== 'record' || !Array.isArray(node.fields)) continue |
| 705 | const kept = [] |
| 706 | for (const field of node.fields) { |
| 707 | if (!field.vendor) { |
| 708 | kept.push(field) |
| 709 | continue |
| 710 | } |
| 711 | anchors.add(field.via) |
| 712 | // The extension point's own type carries a copy of its fields; drop that copy (the anchor |
| 713 | // itself is removed below) and route only the copy that resolved into a real target type. |
| 714 | if (field.via === typeName) continue |
| 715 | const { vendor: ns, via, ...clean } = field |
| 716 | const bucket = (vendor[ns] ??= { extends: {} }) |
| 717 | const entry = (bucket.extends[typeName] ??= { via, fields: [] }) |
| 718 | entry.fields.push(clean) |
| 719 | } |
| 720 | node.fields = kept |
| 721 | } |
| 722 | for (const name of anchors) { |
| 723 | const anchor = types[name] |
| 724 | if (anchor && anchor.kind === 'record' && (anchor.fields?.length ?? 0) === 0) delete types[name] |
| 725 | } |
| 726 | return vendor |
| 727 | } |
| 728 | |
| 729 | /** |
| 730 | * Fail-closed validation: every type reference resolves, and no type projects to |