@techspokes/typescript-wsdl-client 1.4.0 → 1.5.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.
@@ -0,0 +1,60 @@
1
+ import type { AttributeNode, ElementNode } from './canonicalGraph.js';
2
+ import type { SemanticContext } from './semanticContext.js';
3
+ import type { ExpandedName } from '../loader/orderedSyntax.js';
4
+ /** Optional additive metadata shared by inspection, direct runtimes and generated HTTP failures.
5
+ * @since 1.5.0
6
+ * @constraints Resource and universe identifiers are opaque; source locators remain trusted local data.
7
+ */
8
+ export interface CheckedPayloadDiagnostic {
9
+ readonly version: 'checked-payload-diagnostic-v1';
10
+ readonly universe: string;
11
+ readonly resource: string;
12
+ readonly digest: string;
13
+ readonly declaration: {
14
+ readonly kind: 'element' | 'attribute';
15
+ readonly name: ExpandedName;
16
+ };
17
+ readonly owner?: {
18
+ readonly kind: string;
19
+ readonly name?: ExpandedName;
20
+ };
21
+ readonly datatype?: ExpandedName;
22
+ readonly builtin?: ExpandedName;
23
+ readonly location: string;
24
+ readonly unknownField?: true;
25
+ readonly truncated?: true;
26
+ }
27
+ type Evidence = Readonly<{
28
+ context: SemanticContext;
29
+ declaration: ElementNode | AttributeNode;
30
+ }>;
31
+ /** Format only a schema-established expanded name, with a bounded occurrence index.
32
+ * @param name Name supplied by the native schema owner, never an unknown payload name.
33
+ * @param index One-based occurrence index.
34
+ * @returns A bounded readable location segment.
35
+ */
36
+ export declare function diagnosticName(name: ExpandedName, index?: number): string;
37
+ /** Append a known-field segment while retaining an explicit marker for omitted ancestor information.
38
+ * @param parent Bounded location already built from native schema fields.
39
+ * @param segment Native known-field segment or fixed diagnostic marker.
40
+ * @returns At most 2048 characters, including an explicit truncation marker when necessary.
41
+ */
42
+ export declare function appendDiagnosticLocation(parent: string, segment: string): string;
43
+ /** Publish safe diagnostic facts while retaining native source ownership privately.
44
+ * @param context Actual semantic owner of the failed declaration.
45
+ * @param declaration Actual declaration selected by native matching.
46
+ * @param location Location built from matched schema fields.
47
+ * @param options Optional scalar builtin and unknown-field marker.
48
+ * @returns Bounded, immutable, value-redacted metadata with no native handles or source URI.
49
+ */
50
+ export declare function createPayloadDiagnostic(context: SemanticContext, declaration: ElementNode | AttributeNode, location: string, options?: Readonly<{
51
+ builtin?: ExpandedName;
52
+ unknownField?: boolean;
53
+ }>): CheckedPayloadDiagnostic;
54
+ /** Read native provenance only for a diagnostic emitted in this process.
55
+ * @param diagnostic Exact issued diagnostic object.
56
+ * @returns Native declaration ownership or undefined for copied/public data.
57
+ * @constraints This trusted internal seam is not a public HTTP or JSON authority surface.
58
+ */
59
+ export declare function inspectPayloadDiagnostic(diagnostic: CheckedPayloadDiagnostic): Evidence | undefined;
60
+ export {};
@@ -0,0 +1,73 @@
1
+ /** Bounded value-redacted facts emitted by the native declaration owner.
2
+ * @since 1.5.0
3
+ * @constraints Display metadata never establishes validation or correction authority.
4
+ */
5
+ import { createHash } from 'node:crypto';
6
+ const evidence = new WeakMap();
7
+ const hash = (value) => createHash('sha256').update(value).digest('hex');
8
+ /** Format only a schema-established expanded name, with a bounded occurrence index.
9
+ * @param name Name supplied by the native schema owner, never an unknown payload name.
10
+ * @param index One-based occurrence index.
11
+ * @returns A bounded readable location segment.
12
+ */
13
+ export function diagnosticName(name, index = 1) {
14
+ return `{${name.namespace.slice(0, 256)}}${name.local.slice(0, 256)}[${index > 100_000 ? '>100000' : index}]`
15
+ + (name.namespace.length > 256 || name.local.length > 256 ? '[truncated]' : '');
16
+ }
17
+ /** Append a known-field segment while retaining an explicit marker for omitted ancestor information.
18
+ * @param parent Bounded location already built from native schema fields.
19
+ * @param segment Native known-field segment or fixed diagnostic marker.
20
+ * @returns At most 2048 characters, including an explicit truncation marker when necessary.
21
+ */
22
+ export function appendDiagnosticLocation(parent, segment) {
23
+ if (parent.endsWith('/[truncated]'))
24
+ return parent;
25
+ const location = parent + '/' + segment;
26
+ return location.length > 2048 ? location.slice(0, 2036) + '/[truncated]' : location;
27
+ }
28
+ /** Publish safe diagnostic facts while retaining native source ownership privately.
29
+ * @param context Actual semantic owner of the failed declaration.
30
+ * @param declaration Actual declaration selected by native matching.
31
+ * @param location Location built from matched schema fields.
32
+ * @param options Optional scalar builtin and unknown-field marker.
33
+ * @returns Bounded, immutable, value-redacted metadata with no native handles or source URI.
34
+ */
35
+ export function createPayloadDiagnostic(context, declaration, location, options = {}) {
36
+ let truncated = location.length > 2048 || location.includes('[truncated]') || location.includes('[>100000]');
37
+ const name = (value) => {
38
+ if (value.namespace.length > 256 || value.local.length > 256)
39
+ truncated = true;
40
+ return Object.freeze({ namespace: value.namespace.slice(0, 256), local: value.local.slice(0, 256) });
41
+ };
42
+ let ownerId = context.owner(declaration.id), owner;
43
+ for (let depth = 0; ownerId && depth < 32; depth++) {
44
+ const found = context.lookup(ownerId);
45
+ if (found.kind !== 'found')
46
+ break;
47
+ if (found.node.identity.kind === 'global') {
48
+ owner = Object.freeze({ kind: found.node.kind, name: name(found.node.identity.name) });
49
+ break;
50
+ }
51
+ ownerId = context.owner(ownerId);
52
+ }
53
+ const resolved = context.resolve(declaration.type);
54
+ const datatype = resolved.kind === 'builtin' ? resolved.name : resolved.kind === 'found'
55
+ && resolved.node.identity.kind === 'global' ? resolved.node.identity.name : undefined;
56
+ const source = declaration.context.source;
57
+ const result = Object.freeze({ version: 'checked-payload-diagnostic-v1',
58
+ universe: `universe-${hash(context.contextId)}`, resource: `resource-${hash(source.uri)}`, digest: source.digest,
59
+ declaration: Object.freeze({ kind: declaration.kind, name: name(declaration.name) }),
60
+ ...(owner ? { owner } : {}), ...(datatype ? { datatype: name(datatype) } : {}),
61
+ ...(options.builtin ? { builtin: name(options.builtin) } : {}), location: location.slice(0, 2048),
62
+ ...(options.unknownField ? { unknownField: true } : {}), ...(truncated ? { truncated: true } : {}) });
63
+ evidence.set(result, { context, declaration });
64
+ return result;
65
+ }
66
+ /** Read native provenance only for a diagnostic emitted in this process.
67
+ * @param diagnostic Exact issued diagnostic object.
68
+ * @returns Native declaration ownership or undefined for copied/public data.
69
+ * @constraints This trusted internal seam is not a public HTTP or JSON authority surface.
70
+ */
71
+ export function inspectPayloadDiagnostic(diagnostic) {
72
+ return evidence.get(diagnostic);
73
+ }
@@ -1,4 +1,5 @@
1
1
  import type { SchemaInput } from "../loader/schemaInput.js";
2
+ import type { SyntaxSource } from "../loader/orderedSyntax.js";
2
3
  import type { OfflineSchemaResource } from "../loader/schemaResources.js";
3
4
  import type { GraphNode, NodeId } from "./canonicalGraph.js";
4
5
  export type SourceCorrectionSelector = Readonly<{
@@ -42,6 +43,10 @@ export type SourceCorrectionInventoryNode = Readonly<{
42
43
  parent?: NodeId;
43
44
  chameleon: boolean;
44
45
  ambiguousContext: boolean;
46
+ declaredType?: Readonly<{
47
+ namespace: string;
48
+ local: string;
49
+ }>;
45
50
  }>;
46
51
  export type SourceCorrectionInventory = Readonly<{
47
52
  version: 1;
@@ -88,7 +93,19 @@ type InsertCorrection = Readonly<{
88
93
  position: InsertPosition;
89
94
  element: InsertElement;
90
95
  }>;
91
- type Correction = OccursCorrection | WildcardCorrection | InsertCorrection;
96
+ type DeclaredTypeCorrection = Readonly<{
97
+ kind: "set-declared-type";
98
+ target: SourceCorrectionSelector;
99
+ expectedType: Readonly<{
100
+ namespace: string;
101
+ local: string;
102
+ }>;
103
+ type: Readonly<{
104
+ namespace: string;
105
+ local: string;
106
+ }>;
107
+ }>;
108
+ type Correction = OccursCorrection | WildcardCorrection | InsertCorrection | DeclaredTypeCorrection;
92
109
  export type SourceCorrectionProvenance = Readonly<{
93
110
  version: 1;
94
111
  canonicalRecipeDigest: string;
@@ -107,6 +124,38 @@ export type PreparedCorrectedSourceInputs = Readonly<{
107
124
  inventory: SourceCorrectionInventory;
108
125
  resources: ReadonlyMap<string, OfflineSchemaResource>;
109
126
  provenance: SourceCorrectionProvenance;
127
+ originalTargets: readonly Readonly<{
128
+ uri: string;
129
+ effectiveDigest: string;
130
+ effectiveStart: number;
131
+ namespace: string;
132
+ kind: GraphNode['kind'];
133
+ ambiguous: boolean;
134
+ target: SourceCorrectionSelector;
135
+ declaredType?: Readonly<{
136
+ namespace: string;
137
+ local: string;
138
+ }>;
139
+ }>[];
140
+ }>;
141
+ /** Recover an original target from the actual edit spans, never an effective ordinal path.
142
+ * @param prepared Native correction result retaining the original pinned inventory and edit correspondence.
143
+ * @param source Effective declaration source obtained from its native semantic owner.
144
+ * @param namespace Effective schema interpretation namespace.
145
+ * @param kind Actual declaration kind.
146
+ * @returns A pinned original selector, or an explicit reason when correspondence is unavailable or ambiguous.
147
+ * @constraints This trusted local authoring result contains source locators and must not enter HTTP diagnostics.
148
+ */
149
+ export declare function recoverOriginalSourceCorrectionTarget(prepared: PreparedCorrectedSourceInputs, source: SyntaxSource, namespace: string, kind: GraphNode['kind']): Readonly<{
150
+ kind: 'available';
151
+ target: SourceCorrectionSelector;
152
+ declaredType?: Readonly<{
153
+ namespace: string;
154
+ local: string;
155
+ }>;
156
+ } | {
157
+ kind: 'unavailable';
158
+ reason: 'original-target-not-established' | 'ambiguous-original-target';
110
159
  }>;
111
160
  export declare class SourceCorrectionError extends Error {
112
161
  readonly category: "invalid-recipe" | "stale-source" | "ambiguous-target" | "unsupported-target" | "resource-limit" | "cancelled";
@@ -1,10 +1,11 @@
1
1
  /** Explicit, source-pinned XML corrections for loaded schema inputs. */
2
2
  import { createHash } from "node:crypto";
3
3
  import { copyOriginalSchemaResource } from "../loader/schemaInput.js";
4
- import { decodeMetadataXml, isXmlNCName, syntaxElements, XSD_NAMESPACE } from "../loader/orderedSyntax.js";
4
+ import { decodeMetadataXml, isXmlNCName, syntaxAttribute, syntaxElements, XSD_NAMESPACE } from "../loader/orderedSyntax.js";
5
5
  import { DEFAULT_GRAPH_NODES, GraphError, isBuiltinType } from "./canonicalGraph.js";
6
6
  import { buildCanonicalGraph } from "./buildCanonicalGraph.js";
7
7
  import { namespaceAllows, wildcardNamespaces } from './wildcardAlgebra.js';
8
+ import { normalizeWhitespace } from '../util/xmlWhitespace.js';
8
9
  const RECIPE_VERSION = 1;
9
10
  const MAX_RECIPE_BYTES = 256 * 1024;
10
11
  const MAX_RECIPE_DEPTH = 32;
@@ -13,6 +14,22 @@ const MAX_OUTPUT_GROWTH = 2 * 1024 * 1024;
13
14
  const MAX_SYNTAX_INDEX_NODES = DEFAULT_GRAPH_NODES;
14
15
  const MAX_LEXICAL_SCAN = 64 * 1024 * 1024;
15
16
  const HASH = /^[a-f0-9]{64}$/;
17
+ /** Recover an original target from the actual edit spans, never an effective ordinal path.
18
+ * @param prepared Native correction result retaining the original pinned inventory and edit correspondence.
19
+ * @param source Effective declaration source obtained from its native semantic owner.
20
+ * @param namespace Effective schema interpretation namespace.
21
+ * @param kind Actual declaration kind.
22
+ * @returns A pinned original selector, or an explicit reason when correspondence is unavailable or ambiguous.
23
+ * @constraints This trusted local authoring result contains source locators and must not enter HTTP diagnostics.
24
+ */
25
+ export function recoverOriginalSourceCorrectionTarget(prepared, source, namespace, kind) {
26
+ const matches = prepared.originalTargets.filter(item => item.uri === source.uri && item.effectiveDigest === source.digest
27
+ && item.effectiveStart === source.start.offset && item.namespace === namespace && item.kind === kind);
28
+ if (matches.length !== 1 || matches[0].ambiguous)
29
+ return { kind: 'unavailable',
30
+ reason: matches.length > 1 || matches[0]?.ambiguous ? 'ambiguous-original-target' : 'original-target-not-established' };
31
+ return { kind: 'available', target: matches[0].target, ...(matches[0].declaredType ? { declaredType: matches[0].declaredType } : {}) };
32
+ }
16
33
  export class SourceCorrectionError extends Error {
17
34
  category;
18
35
  constructor(category, message) {
@@ -127,6 +144,13 @@ function checkOccurs(min, max, label) {
127
144
  if (max !== "unbounded" && (min.length > max.length || (min.length === max.length && min > max)))
128
145
  fail("invalid-recipe", `${label} minimum exceeds maximum`);
129
146
  }
147
+ function expandedType(value, label) {
148
+ const item = record(value, ["namespace", "local"], label);
149
+ const namespace = text(item.namespace, `${label}.namespace`, true), local = text(item.local, `${label}.local`);
150
+ if (!isXmlNCName(local))
151
+ fail("invalid-recipe", `${label}.local must be an XML NCName`);
152
+ return Object.freeze({ namespace, local });
153
+ }
130
154
  function parseRecipe(value, signal) {
131
155
  boundedJsonSize(value, signal);
132
156
  const root = record(value, ["version", "resources", "corrections"], "recipe");
@@ -145,6 +169,12 @@ function parseRecipe(value, signal) {
145
169
  if (!raw || typeof raw !== "object" || Array.isArray(raw))
146
170
  fail("invalid-recipe", `corrections[${index}] must be an object`);
147
171
  const kind = raw.kind;
172
+ if (kind === "set-declared-type") {
173
+ const item = record(raw, ["kind", "target", "expectedType", "type"], `corrections[${index}]`);
174
+ return Object.freeze({ kind, target: selector(item.target, `corrections[${index}].target`),
175
+ expectedType: expandedType(item.expectedType, `corrections[${index}].expectedType`),
176
+ type: expandedType(item.type, `corrections[${index}].type`) });
177
+ }
148
178
  if (kind === "set-occurs") {
149
179
  const item = record(raw, ["kind", "target", "min", "max"], `corrections[${index}]`);
150
180
  const min = occurrence(item.min, `corrections[${index}].min`, false), max = occurrence(item.max, `corrections[${index}].max`, true);
@@ -312,9 +342,10 @@ function targetMap(inputs, signal) {
312
342
  if (old.node.context.effectiveNamespace !== node.context.effectiveNamespace || old.node.context.source.path !== source.path) {
313
343
  fail("ambiguous-target", "One canonical identity has conflicting schema contexts");
314
344
  }
345
+ old.universes.push(graph);
315
346
  continue;
316
347
  }
317
- targets.set(key, { node, syntax, ambiguousContext: false });
348
+ targets.set(key, { node, syntax, ambiguousContext: false, universes: [graph] });
318
349
  }
319
350
  }
320
351
  for (const target of targets.values()) {
@@ -350,7 +381,9 @@ function targetMap(inputs, signal) {
350
381
  ...(nodeName(node) ? { name: nodeName(node) } : {}), syntaxName: syntax.name, uri: source.uri, digest: source.digest, path: source.path,
351
382
  start: source.start.offset, end: source.end.offset, attributes: rawAttributes, ...(node.kind === "particle" ? { occurs: node.occurs } : {}),
352
383
  ...(wildcard ? { wildcard } : {}), ...(children ? { children } : {}), ...(parents.has(node.id) ? { parent: parents.get(node.id) } : {}),
353
- chameleon: Boolean(node.context.chameleon || contextChameleon.get(sourceKey(source.uri, source.digest, source.path))), ambiguousContext });
384
+ chameleon: Boolean(node.context.chameleon || contextChameleon.get(sourceKey(source.uri, source.digest, source.path))), ambiguousContext,
385
+ ...((node.kind === 'element' || node.kind === 'attribute') && syntaxAttribute(syntax, 'type') !== undefined && node.type.kind !== 'local'
386
+ ? { declaredType: node.type.name } : {}) });
354
387
  nodes.push(entry);
355
388
  }
356
389
  nodes.sort((a, b) => compareText(a.uri, b.uri) || compareText(a.path, b.path) || compareText(a.id, b.id));
@@ -588,6 +621,73 @@ function validateTargetType(qnameValue, targets) {
588
621
  if (!exists)
589
622
  fail("unsupported-target", "Inserted element type must refer to an existing named type or XML Schema builtin");
590
623
  }
624
+ /** Check a replacement in each declaring source universe and schema interpretation.
625
+ * @param target Original authored declaration and its independent acquired universes.
626
+ * @param replacement Expanded replacement type, never a payload-derived guess.
627
+ * @param signal Cooperative cancellation signal.
628
+ * @param budget Shared bounded correction work allowance.
629
+ * @returns Nothing when every origin permits the existing type reference.
630
+ * @throws SourceCorrectionError For unavailable, illegal, unimported or ambiguous types, cancellation or exhausted work.
631
+ * @constraints An independent companion cannot lend its types or import permissions to this declaration.
632
+ */
633
+ function validateDeclaredType(target, replacement, signal, budget) {
634
+ const charge = () => {
635
+ checkAbort(signal);
636
+ if (--budget.remaining < 0)
637
+ fail('resource-limit', 'Declared-type correction exceeds its work limit');
638
+ };
639
+ if (replacement.namespace === XSD_NAMESPACE && isBuiltinType(replacement.local)) {
640
+ if (target.node.kind === 'attribute' && replacement.local === 'anyType')
641
+ fail('unsupported-target', 'Attribute declared type must be simple');
642
+ return;
643
+ }
644
+ for (const graph of target.universes) {
645
+ charge();
646
+ const matches = graph.nodes.filter(node => {
647
+ charge();
648
+ return (node.kind === 'simpleType' || node.kind === 'complexType') && node.identity.kind === 'global'
649
+ && node.identity.name.namespace === replacement.namespace && node.identity.name.local === replacement.local;
650
+ });
651
+ if (matches.length !== 1)
652
+ fail(matches.length > 1 ? 'ambiguous-target' : 'unsupported-target', 'Replacement type must be an existing named type in the target source universe');
653
+ if (target.node.kind === 'attribute' && matches[0].kind !== 'simpleType')
654
+ fail('unsupported-target', 'Attribute declared type must be simple');
655
+ const imports = new Map();
656
+ const importSyntax = new Map();
657
+ for (const retained of graph.schemaRetained) {
658
+ charge();
659
+ if (retained.syntax.name.namespace === XSD_NAMESPACE && retained.syntax.name.local === 'import') {
660
+ const source = retained.syntax.source;
661
+ importSyntax.set(sourceKey(source.uri, source.digest, source.path), retained.syntax);
662
+ }
663
+ }
664
+ for (const edge of graph.loading.edges) {
665
+ charge();
666
+ if (edge.kind !== 'import')
667
+ continue;
668
+ const syntax = importSyntax.get(sourceKey(edge.source.uri, edge.source.digest, edge.source.path));
669
+ if (!syntax)
670
+ fail('unsupported-target', 'Replacement type import prerequisite is unavailable');
671
+ const namespaces = imports.get(edge.from) ?? new Set();
672
+ namespaces.add(normalizeWhitespace(syntaxAttribute(syntax, 'namespace') ?? '', 'collapse'));
673
+ imports.set(edge.from, namespaces);
674
+ }
675
+ const source = target.node.context.source;
676
+ const origins = (graph.origins[target.node.id] ?? []).filter(origin => {
677
+ charge();
678
+ const actual = origin.context.source;
679
+ return sourceKey(actual.uri, actual.digest, actual.path) === sourceKey(source.uri, source.digest, source.path);
680
+ });
681
+ if (!origins.length)
682
+ fail('unsupported-target', 'Replacement type declaring interpretation is unavailable');
683
+ for (const origin of origins) {
684
+ charge();
685
+ if (replacement.namespace !== origin.context.effectiveNamespace && !imports.get(origin.interpretation)?.has(replacement.namespace)) {
686
+ fail('unsupported-target', 'Replacement type namespace is not imported by the declaring schema');
687
+ }
688
+ }
689
+ }
690
+ }
591
691
  function decodedResource(source, cache) {
592
692
  const prior = cache.get(source.uri);
593
693
  if (prior !== undefined)
@@ -599,6 +699,30 @@ function decodedResource(source, cache) {
599
699
  function validateCorrection(correction, targets, sources, decoded, claimed, insertSlots, signal, scanBudget = { remaining: MAX_LEXICAL_SCAN }) {
600
700
  const target = targetFor(correction.target, targets), source = checkSelectorResource(correction.target, sources), node = target.node;
601
701
  checkAbort(signal);
702
+ if (correction.kind === 'set-declared-type') {
703
+ if ((node.kind !== 'element' && node.kind !== 'attribute') || syntaxAttribute(target.syntax, 'ref') !== undefined
704
+ || syntaxAttribute(target.syntax, 'type') === undefined || node.type.kind === 'local'
705
+ || syntaxElements(target.syntax, XSD_NAMESPACE).some(child => child.name.local === 'simpleType' || child.name.local === 'complexType')) {
706
+ fail('unsupported-target', 'set-declared-type requires an authored element or attribute declaration with an existing @type; inline and ref forms refuse');
707
+ }
708
+ if (node.type.name.namespace !== correction.expectedType.namespace || node.type.name.local !== correction.expectedType.local) {
709
+ fail('stale-source', 'Declared type does not match expectedType in the original schema context');
710
+ }
711
+ validateDeclaredType(target, correction.type, signal, scanBudget);
712
+ if (node.context.chameleon && correction.type.namespace === '')
713
+ fail('unsupported-target', 'A no-namespace replacement would be reinterpreted by chameleon adoption');
714
+ const claim = `${sourceKey(source.uri, source.digest, node.context.source.path)}:type`;
715
+ if (claimed.has(claim))
716
+ fail('invalid-recipe', 'Conflicting correction for declared type');
717
+ claimed.add(claim);
718
+ const xml = decodedResource(source, decoded), replacement = qname(correction.type.namespace, correction.type.local, node.context.namespaces, new Set());
719
+ const edits = updateStartTag(xml, target, { type: replacement.lexical }, signal, scanBudget);
720
+ if (replacement.declaration) {
721
+ const tag = startTag(xml, node.context.source.start.offset, signal, scanBudget);
722
+ edits.push({ start: tag.close, end: tag.close, text: ` ${replacement.declaration}`, label: `${node.id}:attributes` });
723
+ }
724
+ return { patches: new Map([[source.uri, edits]]), provenance: { kind: correction.kind, target: correction.target } };
725
+ }
602
726
  if (correction.kind === "set-occurs") {
603
727
  if (node.kind !== "particle")
604
728
  fail("unsupported-target", "set-occurs target must be an XSD particle");
@@ -692,9 +816,10 @@ function validateCorrection(correction, targets, sources, decoded, claimed, inse
692
816
  insertSlots.add(slot);
693
817
  return { patches: new Map([[source.uri, [{ start: position, end: replacementEnd ?? position, text, label: `${node.id}:insert` }]]]), provenance: { kind: correction.kind, target: correction.target } };
694
818
  }
695
- function applyPatches(original, patches, signal) {
819
+ function normalizedPatches(patches, signal) {
696
820
  const combined = new Map();
697
821
  for (const patch of patches) {
822
+ checkAbort(signal);
698
823
  const key = `${patch.start}:${patch.end}`;
699
824
  const prior = combined.get(key);
700
825
  if (!prior) {
@@ -706,10 +831,12 @@ function applyPatches(original, patches, signal) {
706
831
  }
707
832
  combined.set(key, { ...prior, text: prior.text + patch.text.replace(/^\s+/, " ") });
708
833
  }
709
- const sorted = [...combined.values()].sort((a, b) => a.start - b.start || a.end - b.end || compareText(a.label, b.label));
834
+ return [...combined.values()].sort((a, b) => a.start - b.start || a.end - b.end || compareText(a.label, b.label));
835
+ }
836
+ function applyPatches(original, patches, signal) {
710
837
  let previousEnd = 0;
711
838
  const chunks = [];
712
- for (const patch of sorted) {
839
+ for (const patch of patches) {
713
840
  checkAbort(signal);
714
841
  if (patch.start < previousEnd || patch.end < patch.start || patch.end > original.length)
715
842
  fail("invalid-recipe", "Correction source spans overlap or exceed the original resource");
@@ -755,12 +882,14 @@ export function prepareCorrectedSourceInputs(inputs, recipeValue, signal) {
755
882
  provenanceCorrections.push(result.provenance);
756
883
  }
757
884
  const resources = new Map(), provenanceResources = [];
885
+ const originalTargets = [];
758
886
  let effectiveBytesTotal = 0;
759
887
  let outputGrowth = 0;
760
888
  const outputLimit = Math.min(...inputs.map(input => input.limits.totalBytes)), singleLimit = Math.min(...inputs.map(input => input.limits.resourceBytes));
761
889
  for (const source of [...sources.values()].sort((a, b) => compareText(a.uri, b.uri))) {
762
890
  checkAbort(signal);
763
- const edits = patches.get(source.uri) ?? [];
891
+ // @constraints Application and original-target correspondence consume exactly the same combined edits.
892
+ const edits = normalizedPatches(patches.get(source.uri) ?? [], signal);
764
893
  const originalText = edits.length ? decodedResource(source, decoded) : "";
765
894
  const updated = edits.length ? applyPatches(originalText, edits, signal) : "";
766
895
  const bytes = edits.length && updated !== originalText ? utf8Output(updated, source.bytes) : source.bytes;
@@ -773,6 +902,26 @@ export function prepareCorrectedSourceInputs(inputs, recipeValue, signal) {
773
902
  if (outputGrowth > MAX_OUTPUT_GROWTH)
774
903
  fail("resource-limit", "Correction output growth exceeds the limit");
775
904
  const effectiveDigest = sha256(bytes), changed = effectiveDigest !== source.digest;
905
+ // @constraints Encoding-declaration rewriting is outside edit-span correspondence; refuse rather than guess.
906
+ if (!edits.length || decodeMetadataXml(bytes) === updated)
907
+ for (const node of inventory.nodes) {
908
+ checkAbort(signal);
909
+ if (node.uri !== source.uri || node.digest !== source.digest)
910
+ continue;
911
+ let effectiveStart = node.start, established = true;
912
+ for (const edit of edits) {
913
+ if (edit.end <= node.start)
914
+ effectiveStart += edit.text.length - (edit.end - edit.start);
915
+ else if (edit.start <= node.start)
916
+ established = false;
917
+ }
918
+ const native = targets.get(selectorKey({ id: node.id, uri: node.uri, digest: node.digest }))?.node;
919
+ const declaredType = native && (native.kind === 'element' || native.kind === 'attribute') && native.type.kind !== 'local' ? native.type.name : undefined;
920
+ if (established)
921
+ originalTargets.push(Object.freeze({ uri: source.uri, effectiveDigest, effectiveStart,
922
+ namespace: node.namespace, kind: node.kind, ambiguous: node.ambiguousContext,
923
+ target: Object.freeze({ id: node.id, uri: node.uri, digest: node.digest }), ...(declaredType ? { declaredType } : {}) }));
924
+ }
776
925
  const resource = Object.freeze({ bytes: new Uint8Array(bytes), digest: effectiveDigest });
777
926
  resources.set(source.uri, resource);
778
927
  for (const alias of source.aliases)
@@ -790,5 +939,5 @@ export function prepareCorrectedSourceInputs(inputs, recipeValue, signal) {
790
939
  }
791
940
  const provenance = Object.freeze({ version: 1, canonicalRecipeDigest: canonicalRecipeDigest(recipe),
792
941
  resources: Object.freeze(provenanceResources), corrections: Object.freeze(provenanceCorrections.map(item => Object.freeze(item))) });
793
- return Object.freeze({ inventory, resources, provenance });
942
+ return Object.freeze({ inventory, resources, provenance, originalTargets: Object.freeze(originalTargets) });
794
943
  }
@@ -0,0 +1,14 @@
1
+ import type { DirectOperationSelector } from '../compiler/directOperation.js';
2
+ type Schema = Readonly<Record<string, unknown>>;
3
+ /** Lower one issued shape while its original reference scope is still available.
4
+ * @param root Generator-owned shape, with at most one root resource and definition dictionary.
5
+ * @param selector Outer operation selector, independent of companion projection contracts.
6
+ * @param role Direction or declared-fault role owning the shape.
7
+ * @param grouped Whether published configured definition names must be preserved.
8
+ * @param components Destination dictionary shared by the operation document.
9
+ * @param check Generation cancellation and semantic resource-accounting checkpoint.
10
+ * @returns A new documentation schema; runtime schemas and literal annotations remain unchanged.
11
+ * @throws TypeError For unexpected resources, references, missing descendants or component collisions.
12
+ */
13
+ export declare function lowerCheckedDocumentation(root: Schema, selector: DirectOperationSelector, role: string, grouped: boolean, components: Record<string, Schema>, check: () => void): Schema;
14
+ export {};
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Relocates generator-owned shape resources into OpenAPI documentation components.
3
+ * @since 1.4.1
4
+ * @constraints This is not a general JSON Schema resource bundler.
5
+ */
6
+ import { createHash } from 'node:crypto';
7
+ const maps = new Set(['properties', 'patternProperties', 'dependentSchemas']);
8
+ const singles = new Set(['additionalProperties', 'additionalItems', 'contains', 'not', 'if', 'then', 'else', 'propertyNames', 'unevaluatedProperties', 'unevaluatedItems']);
9
+ const arrays = new Set(['allOf', 'anyOf', 'oneOf', 'prefixItems']);
10
+ const escape = (value) => encodeURIComponent(value.replace(/~/g, '~0').replace(/\//g, '~1'));
11
+ /** Lower one issued shape while its original reference scope is still available.
12
+ * @param root Generator-owned shape, with at most one root resource and definition dictionary.
13
+ * @param selector Outer operation selector, independent of companion projection contracts.
14
+ * @param role Direction or declared-fault role owning the shape.
15
+ * @param grouped Whether published configured definition names must be preserved.
16
+ * @param components Destination dictionary shared by the operation document.
17
+ * @param check Generation cancellation and semantic resource-accounting checkpoint.
18
+ * @returns A new documentation schema; runtime schemas and literal annotations remain unchanged.
19
+ * @throws TypeError For unexpected resources, references, missing descendants or component collisions.
20
+ */
21
+ export function lowerCheckedDocumentation(root, selector, role, grouped, components, check) {
22
+ check();
23
+ if (root.$id !== undefined && (typeof root.$id !== 'string' || !/^urn:checked-component:shape:[a-f0-9]{64}$/.test(root.$id))) {
24
+ throw new TypeError('Unexpected checked documentation resource');
25
+ }
26
+ const definitions = (root.definitions ?? {});
27
+ // @why #467 Original resource IDs encode selection, not contract equivalence or component ownership.
28
+ const owner = createHash('sha256').update(JSON.stringify([selector.portType.namespace, selector.portType.local,
29
+ selector.binding.namespace, selector.binding.local, selector.operation, role])).digest('hex');
30
+ const names = new Map(Object.keys(definitions).sort().map(name => [name, grouped ? name : `Checked_${owner}_${name}`]));
31
+ const reference = (value) => {
32
+ if (typeof value !== 'string' || !value.startsWith('#/definitions/'))
33
+ throw new TypeError('Unexpected checked documentation reference');
34
+ let pointer;
35
+ try {
36
+ pointer = decodeURIComponent(value.slice(1));
37
+ }
38
+ catch {
39
+ throw new TypeError('Invalid checked documentation pointer');
40
+ }
41
+ const parts = pointer.slice(1).split('/').map(part => {
42
+ if (/~(?![01])/.test(part))
43
+ throw new TypeError('Invalid checked documentation pointer');
44
+ return part.replace(/~1/g, '/').replace(/~0/g, '~');
45
+ });
46
+ const name = names.get(parts[1]);
47
+ if (!name)
48
+ throw new TypeError('Unresolved checked documentation definition');
49
+ let target = root;
50
+ for (const part of parts) {
51
+ check();
52
+ if (!target || typeof target !== 'object' || !Object.hasOwn(target, part))
53
+ throw new TypeError('Unresolved checked documentation descendant');
54
+ target = target[part];
55
+ }
56
+ if (typeof target !== 'boolean' && (!target || typeof target !== 'object' || Array.isArray(target))) {
57
+ throw new TypeError('Checked documentation reference must target a schema');
58
+ }
59
+ return '#/components/schemas/' + escape(name) + parts.slice(2).map(part => '/' + escape(part)).join('');
60
+ };
61
+ const visit = (value, isRoot = false) => {
62
+ check();
63
+ if (typeof value === 'boolean')
64
+ return value;
65
+ if (!value || typeof value !== 'object' || Array.isArray(value))
66
+ throw new TypeError('Unexpected checked documentation schema');
67
+ const record = value, result = Object.create(null);
68
+ for (const [key, item] of Object.entries(record)) {
69
+ check();
70
+ if (key === '$id' && isRoot)
71
+ continue;
72
+ if (key === 'definitions' && isRoot)
73
+ continue;
74
+ if (['$id', '$anchor', '$dynamicAnchor', '$dynamicRef', '$recursiveRef', '$recursiveAnchor', '$defs', 'definitions'].includes(key)) {
75
+ throw new TypeError('Unexpected checked documentation resource');
76
+ }
77
+ if (key === '$ref')
78
+ result[key] = reference(item);
79
+ else if (maps.has(key) || key === 'dependencies') {
80
+ const dictionary = Object.create(null);
81
+ for (const [name, child] of Object.entries(item)) {
82
+ check();
83
+ dictionary[name] = key === 'dependencies' && Array.isArray(child) ? child : visit(child);
84
+ }
85
+ result[key] = dictionary;
86
+ }
87
+ else if (arrays.has(key) || key === 'items' && Array.isArray(item))
88
+ result[key] = item.map(child => visit(child));
89
+ else if (singles.has(key) || key === 'items')
90
+ result[key] = visit(item);
91
+ // @constraints Unknown annotations are literal data, including keyword-shaped application examples.
92
+ else
93
+ result[key] = item;
94
+ }
95
+ return result;
96
+ };
97
+ const lowered = visit(root, true);
98
+ for (const [original, name] of names) {
99
+ const resolved = visit(definitions[original]);
100
+ if (Object.hasOwn(components, name) && JSON.stringify(components[name]) !== JSON.stringify(resolved)) {
101
+ throw new TypeError(`applicationGroups OpenAPI component collision ${name}`);
102
+ }
103
+ components[name] = resolved;
104
+ }
105
+ return lowered;
106
+ }