ObjectDataUtilitiesSpec:
  version: "2.0"
  level: "contract-only"

  Semantic:
    source_of_truth:
      rule: "Only what is explicitly defined in this spec is guaranteed. Anything not defined is undefined by contract."
    empty:
      definition: "undefined only"
      notes:
        - "null, '', NaN, 0, false are valid values"
        - "absence is represented only by undefined"
    value_categories:
      leaf:
        definition: "any non-container value"
        includes:
          - null
          - ""
          - NaN
          - 0
          - false
          - primitive values
          - non-plain objects (Date, Map, Set, Function, class instance, etc.)
      container:
        definition: "plain object or array only"
        excludes:
          - Date
          - Map
          - Set
          - Function
          - class instance
    recursion:
      allowed_on: "container only"
      forbidden_on: "leaf values"

  path:
    type: "string | string[] | null | undefined"

    empty_path:
      when:
        - null
        - undefined
      normalized: []
      meaning: "refer to the value itself (root)"

    flat_path:
      separator: "."
      encoding:
        escape_rules:
          - "\\\\ => \\"
          - "\\. => ."
        allowed_escapes_only: true
        other_backslash_sequences: "treated as literal characters"
      parsing:
        split_rule: "split by unescaped '.'"
        empty_segments: "ignored"

    array_index_key:
      definition: "array index indicator"
      rule: "string consisting of digits only (e.g. '0', '12')"

    normalization:
      string_path:
        trim: true
        empty_string: []
      array_path:
        element_cast: "String(x).trim()"
        empty_elements: "ignored"

  applicability:
    applies_to:
      - Path
      - Guard
      - Read
      - Write
      - Flat
      - CloneMerge
      - Diff
      - Query
      - Convenience

families:
  A_Path:
    types:
      Path: "string | string[] | null | undefined"

    methods:

      PATH_08_escapeKey:
        name: "escapeKey"
        in:
          key: "any"
        out: "string"
        algorithm:
          cast:
            key: "String(key)"
          replace_order:
            - from: "\\"
              to: "\\\\"
            - from: "."
              to: "\\."
        guarantees:
          produces_escapes_only:
            - "\\\\"
            - "\\."
          escape_order_fixed: true

      PATH_08_unescapeKey:
        name: "unescapeKey"
        in:
          key: "any"
        out: "string"
        algorithm:
          cast:
            key: "String(key)"
          parse:
            recognizes_only:
              - sequence: "\\."
                emits: "."
              - sequence: "\\\\"
                emits: "\\"
            other_backslash_sequences: "literal (do not swallow backslash)"
        guarantees:
          unescapes_only:
            - "\\."
            - "\\\\"

      PATH_02_pathToKeys:
        name: "pathToKeys"
        in:
          path: "string"
        out: "string[]"
        algorithm:
          preprocess:
            s: "String(path).trim()"
            empty_string_returns: []
          split:
            separator: "."
            rule: "split by '.' only when '.' is not preceded by an escape-consumed backslash"
          escape_parsing:
            consumes_only:
              - "\\."
              - "\\\\"
            other_backslash_sequences: "literal (keep backslash)"
          empty_segments: "ignored ('' not pushed)"
        guarantees:
          whitespace_only_returns: []
          split_by_unescaped_dot: true
          supports_only_defined_escape_set: true

      PATH_03_keysToPath:
        name: "keysToPath"
        in:
          keys: "any"
        out: "string"
        algorithm:
          normalize_input:
            if_not_array: []
          encode_each_segment:
            cast: "String(k)"
            escape: "PATH_08_escapeKey"
          join:
            separator: "."
            preserves_order: true
        open_Semantic:
          empty_string_segment_behavior: "not defined here (implementation does not filter '')"

      PATH_01_normalizePath:
        name: "normalizePath"
        in:
          path: "Path"
        out: "string[]"
        behavior:
          when_string_array:
            element_cast: "String(x).trim()"
            filter_empty: true
            note: "array is treated as segments; no flat-path split"
          when_string:
            returns: "PATH_02_pathToKeys(String(path))"
          otherwise:
            returns: []

      PATH_01_normalizeFlatPath:
        name: "normalizeFlatPath"
        alias_of: "PATH_01_normalizePath"
        in:
          path: "Path"
        out: "string[]"

      PATH_04_isPathLike:
        name: "isPathLike"
        in:
          value: "any"
        out: "boolean"
        defined_as: "typeof value === 'string' || Array.isArray(value)"

      PATH_05_isArrayIndexKey:
        name: "isArrayIndexKey"
        in:
          key: "any"
        out: "boolean"
        defined_as: "typeof key === 'string' && /^\\d+$/.test(key)"

      PATH_06_splitParentPath:
        name: "splitParentPath"
        in:
          path: "string"
        out:
          parentPath: "string"
          key: "string"
        algorithm:
          keys: "PATH_02_pathToKeys({path})"
          when_no_keys:
            key: ""
            parentPath: ""
          otherwise:
            key: "last(keys)"
            parentPath: "PATH_03_keysToPath(keys_without_last)"

      PATH_07_joinPath:
        name: "joinPath"
        in:
          parts: "Path[]"
        out: "string"
        algorithm:
          normalize_input:
            if_not_array: []
          keys:
            compute: "parts.flatMap(p => PATH_01_normalizePath({path:p}))"
          out: "PATH_03_keysToPath({keys})"

    invariants:
      key_roundtrip:
        statement: "PATH_08_unescapeKey(PATH_08_escapeKey(k)) === k"
        scope: "any string k"
      path_roundtrip_without_empty_segments:
        statement: "PATH_02_pathToKeys(PATH_03_keysToPath(keys)) deepEqual keys"
        scope: "keys do not contain empty segments ''"

  H_Guard:
    methods:

      GUARD_01_isPlainObject:
        name: "isPlainObject"
        in:
          value: "any"
        out: "boolean"
        algorithm:
          tag_check: "Object.prototype.toString.call(value) === '[object Object]'"
          prototype_check:
            allowed:
              - "Object.prototype"
              - "null"
        defined_as: "tag_check && prototype_check"

      GUARD_01_isObjStrict:
        name: "isObjStrict"
        alias_of: "GUARD_01_isPlainObject"
        in:
          value: "any"
        out: "boolean"

      GUARD_01_isStrictObj:
        name: "isStrictObj"
        alias_of: "GUARD_01_isPlainObject"
        in:
          value: "any"
        out: "boolean"

      GUARD_02_isContainer:
        name: "isContainer"
        in:
          value: "any"
        out: "boolean"
        defined_as:
          any_of:
            - "Array.isArray(value) === true"
            - "GUARD_01_isPlainObject(value) === true"

      GUARD_03_ensureContainerForNextKey:
        name: "ensureContainerForNextKey"
        in:
          nextKey: "any"
        out: "Object|Array"
        algorithm:
          key_cast: "String(nextKey)"
          decision:
            when_array_index_key: "GUARD_05_isArrayIndexKey(key) === true"
            then: "[]"
            otherwise: "{}"

      GUARD_04_coerceContainer:
        name: "coerceContainer"
        in:
          value: "any"
          nextKey: "any"
        out: "Object|Array"
        behavior:
          if_container: "return value"
          otherwise: "return GUARD_03_ensureContainerForNextKey(nextKey)"
        guarantees:
          overwrite_non_container: true
          no_fallback: true

      GUARD_05_isStructuredClonable:
        name: "isStructuredClonable"
        in:
          value: "any"
        out: "boolean"
        scope:
          serves_only: "MERGE_01_2 cloneStructuredValue"
        algorithm:
          primitive_allowed:
            - "null"
            - "boolean"
            - "string"
            - "number && Number.isFinite(value)"
          immediate_reject:
            - "undefined"
            - "function"
            - "symbol"
            - "bigint"
          object_handling:
            allowed_containers:
              - "array"
              - "plain object"
            reject_if:
              - "not array and not plain object"
              - "has symbol keys"
              - "cycle detected (WeakSet)"
          traversal:
            array:
              rule: "every element must be structured clonable"
            object:
              keys_source: "Object.keys"
              rule: "every value must be structured clonable"
        open_Semantic:
          array_holes:
            note: "array holes follow JS Array.prototype.every Semantic"
          property_descriptors:
            note: "non-enumerable properties not inspected"

  B_Read:
    dependencies:
      - "A_Path.PATH_01_normalizePath"
      - "H_Guard.GUARD_03_ensureContainerForNextKey (pickByPaths internal only)"

    methods:

      READ_01_getByPath:
        name: "getByPath"
        in:
          obj: "any"
          path: "A_Path.types.Path"
        out: "any"
        algorithm:
          keys: "A_Path.PATH_01_normalizePath({path})"
          when_empty_keys: "return obj"
          traverse:
            for_each_key_k:
              if_cur_is_nullish: "return undefined"
              else: "cur = cur[k]"
          return: "cur"
        open_Semantic:
          non_container_mid_nodes:
            note: "if cur is not null/undefined, property access follows JS behavior (may yield undefined)"

      READ_02_hasByPath:
        name: "hasByPath"
        in:
          obj: "any"
          path: "A_Path.types.Path"
        out: "boolean"
        defined_as: "READ_01_getByPath(obj, path) !== undefined"

      READ_03_getOr:
        name: "getOr"
        in:
          obj: "any"
          path: "A_Path.types.Path"
          defaultValue: "any"
        out: "any"
        algorithm:
          v: "READ_01_getByPath(obj, path)"
          if_v_is_undefined: "return defaultValue"
          otherwise: "return v"
        guarantees:
          default_applies_only_when: "value === undefined"

      READ_04_pickByPaths:
        name: "pickByPaths"
        in:
          obj: "any"
          paths: "Array<A_Path.types.Path>"
        out: "Object"
        algorithm:
          normalize_paths_input:
            if_not_array: []
          out_init: "{}"
          loop_each_p_in_paths:
            keys: "A_Path.PATH_01_normalizePath({path:p})"
            if_empty_keys: "continue"
            v: "READ_01_getByPath(obj, keys)"
            if_v_is_undefined: "continue"
            write_to_out:
              strategy: "mutating on out only"
              set_by_keys_mut:
                for_each_key_k_in_keys:
                  if_last_key: "cur[k] = v; done"
                  else:
                    nextKey: "keys[i+1]"
                    existed: "cur[k]"
                    reuse_when:
                      all_true:
                        - "existed != null"
                        - "Array.isArray(existed) || Object.prototype.toString.call(existed) === '[object Object]'"
                    otherwise_create:
                      container: "H_Guard.GUARD_03_ensureContainerForNextKey({nextKey:String(nextKey)})"
                      overwrite: true
                    advance: "cur = cur[k]"
        guarantees:
          skips_root_path: true
          skips_undefined_results: true
        open_Semantic:
          conflicts_and_overwrites:
            note: "conflicts (e.g. 'a' then 'a.b') resolved by iteration order and overwrite-on-build behavior"
          plain_object_check_in_out:
            note: "out intermediate reuse uses toString '[object Object]' only (prototype not checked)"

      READ_05_pluckByPaths:
        name: "pluckByPaths"
        in:
          list: "any"
          path: "A_Path.types.Path"
        out: "any[]"
        algorithm:
          xs: "Array.isArray(list) ? list : []"
          return: "xs.map(x => READ_01_getByPath(x, path))"

  C_Write:
    dependencies:
      - "A_Path.PATH_01_normalizePath"
      - "A_Path.PATH_05_isArrayIndexKey"
      - "H_Guard.GUARD_02_isContainer"
      - "H_Guard.GUARD_03_ensureContainerForNextKey"
      - "H_Guard.GUARD_04_coerceContainer"
      - "B_Read.READ_01_getByPath"

    notes:
      empty_value: "undefined only"

    methods:

      WRITE_01_setByPath:
        name: "setByPath"
        in:
          target: "any"
          path: "A_Path.types.Path"
          value: "any"
        out: "any"
        Semantic: "immutable"
        algorithm:
          keys: "A_Path.PATH_01_normalizePath({path})"
          when_empty_keys: "return value"
          root_coercion_immutable:
            firstKey: "String(keys[0])"
            if_target_is_container: "shallow clone (array: slice; object: spread)"
            otherwise: "H_Guard.GUARD_03_ensureContainerForNextKey({nextKey:firstKey})"
          write_along_path:
            per_level_key_cast: "String(k)"
            non_last_segment:
              srcChild: "src == null ? undefined : src[k]"
              dstChild:
                if_srcChild_is_container: "shallow clone (array: slice; object: spread)"
                otherwise: "H_Guard.GUARD_03_ensureContainerForNextKey({nextKey})"
              assign: "dst[k] = dstChild"
              advance: "src = srcChild; dst = dstChild"
            last_segment:
              assign: "dst[lastKey] = value"
              return: "root"
        guarantees:
          replaces_root_when_empty_path: true
          container_inference_only_for_missing_or_non_container_nodes: true
        open_Semantic:
          numeric_string_key_as_index:
            note: "array-index detection is used only for container inference; assignment uses string property set"

      WRITE_01_2_setByPathMut:
        name: "setByPathMut"
        in:
          target: "any"
          path: "A_Path.types.Path"
          value: "any"
        out: "any"
        Semantic: "mutating (on coerced root subtree)"
        algorithm:
          keys: "A_Path.PATH_01_normalizePath({path})"
          when_empty_keys: "return value"
          root_coercion_mutating:
            firstKey: "String(keys[0])"
            root: "H_Guard.GUARD_04_coerceContainer({value:target, nextKey:firstKey})"
          write_along_path:
            per_level_key_cast: "String(k)"
            non_last_segment:
              nextKey: "String(keys[i+1])"
              next: "H_Guard.GUARD_04_coerceContainer({value:cur[k], nextKey})"
              assign: "cur[k] = next"
              advance: "cur = next"
            last_segment:
              assign: "cur[k] = value"
              return: "root"
        open_Semantic:
          root_reference_when_target_not_container:
            note: "when target is not a container, returned root is a newly inferred container"

      WRITE_02_setManyByPaths:
        name: "setManyByPaths"
        in:
          target: "any"
          entries: "any"
        out: "any"
        Semantic: "immutable"
        algorithm:
          xs: "Array.isArray(entries) ? entries : []"
          reduce:
            init: "target"
            step: "acc = WRITE_01_setByPath({target:acc, path:e?.path, value:e?.value})"
          return: "acc"

      WRITE_03_ensureByPath:
        name: "ensureByPath"
        in:
          target: "any"
          path: "A_Path.types.Path"
          init: "any"
        out:
          nextTarget: "any"
          value: "any"
        Semantic: "immutable"
        algorithm:
          cur: "B_Read.READ_01_getByPath(target, path)"
          exists: "cur !== undefined"
          when_exists:
            nextTarget: "target"
            value: "cur"
          otherwise:
            nextTarget: "WRITE_01_setByPath({target, path, value:init})"
            value: "init"
        guarantees:
          initializes_only_when: "current value === undefined"

      WRITE_04_updateByPath:
        name: "updateByPath"
        in:
          target: "any"
          path: "A_Path.types.Path"
          fn: "Function"
        out:
          nextTarget: "any"
          value: "any"
        Semantic: "immutable"
        algorithm:
          oldValue: "B_Read.READ_01_getByPath(target, path)"
          newValue: "fn(oldValue)"
          nextTarget: "WRITE_01_setByPath({target, path, value:newValue})"
          return:
            nextTarget: "nextTarget"
            value: "newValue"
        open_Semantic:
          fn_throw:
            note: "error propagation follows JS behavior (not defined here)"

      WRITE_05_unsetByPath:
        name: "unsetByPath"
        in:
          target: "any"
          path: "A_Path.types.Path"
        out: "any"
        Semantic: "immutable"
        algorithm:
          keys: "A_Path.PATH_01_normalizePath({path})"
          when_empty_keys: "return target"
          parentKeys: "keys.slice(0, keys.length - 1)"
          lastKey: "String(keys[keys.length - 1])"
          parent:
            when_parentKeys_empty: "target"
            otherwise: "B_Read.READ_01_getByPath(target, parentKeys)"
          when_parent_nullish: "return target"
          build_nextTarget:
            when_parentKeys_empty:
              nextTarget: "coerce root immutably (container clone or inferred by firstKey)"
            otherwise:
              clonedParent:
                if_parent_is_container: "shallow clone (array: slice; object: spread)"
                otherwise: "H_Guard.GUARD_03_ensureContainerForNextKey({nextKey:lastKey})"
              nextTarget: "WRITE_01_setByPath({target, path:parentKeys, value:clonedParent})"
          nextParent:
            when_parentKeys_empty: "nextTarget"
            otherwise: "B_Read.READ_01_getByPath(nextTarget, parentKeys)"
          remove_last:
            if_array_and_index_key:
              condition:
                all_true:
                  - "Array.isArray(nextParent) === true"
                  - "A_Path.PATH_05_isArrayIndexKey({key:lastKey}) === true"
              action: "nextParent.splice(Number(lastKey), 1)"
            otherwise:
              action: "delete nextParent[lastKey]"
          return: "nextTarget"
        open_Semantic:
          array_splice_edge_cases:
            note: "splice behavior for out-of-range / holes follows JS behavior"

      WRITE_06_renameByPath:
        name: "renameByPath"
        in:
          target: "any"
          from: "A_Path.types.Path"
          to: "A_Path.types.Path"
        out: "any"
        Semantic: "immutable"
        algorithm:
          v: "B_Read.READ_01_getByPath(target, from)"
          when_v_is_undefined: "return target"
          otherwise:
            a: "WRITE_01_setByPath({target, path:to, value:v})"
            return: "WRITE_05_unsetByPath({target:a, path:from})"

      WRITE_07_pushByPath:
        name: "pushByPath"
        in:
          target: "any"
          path: "A_Path.types.Path"
          item: "any"
        out:
          nextTarget: "any"
          value: "number"
        Semantic: "immutable"
        algorithm:
          cur: "B_Read.READ_01_getByPath(target, path)"
          nextArr: "Array.isArray(cur) ? cur.slice() : []"
          push: "nextArr.push(item)"
          nextTarget: "WRITE_01_setByPath({target, path, value:nextArr})"
          return:
            nextTarget: "nextTarget"
            value: "nextArr.length"

      WRITE_08_removeAtByPath:
        name: "removeAtByPath"
        in:
          target: "any"
          path: "A_Path.types.Path"
          index: "number"
        out:
          nextTarget: "any"
          value: "any"
        Semantic: "immutable"
        algorithm:
          cur: "B_Read.READ_01_getByPath(target, path)"
          arr: "Array.isArray(cur) ? cur.slice() : []"
          i: "Number(index)"
          removed: "(i >= 0 && i < arr.length) ? arr.splice(i, 1)[0] : undefined"
          nextTarget: "WRITE_01_setByPath({target, path, value:arr})"
          return:
            nextTarget: "nextTarget"
            value: "removed"

  D_FlattenUnflatten:
    dependencies:
      - "H_Guard.GUARD_02_isContainer"
      - "H_Guard.GUARD_03_ensureContainerForNextKey"
      - "A_Path.PATH_03_keysToPath"
      - "A_Path.PATH_02_pathToKeys"
      - "C_Write.WRITE_01_setByPath"

    methods:

      FLAT_03_toFlatPairs:
        name: "toFlatPairs"
        in:
          obj: "any"
        out: "Array<{ path:string, value:any }>"
        traversal:
          mode: "DFS"
          recurse_only_when: "H_Guard.GUARD_02_isContainer(value) === true"
          root_keys_init: []
        leaf_rules:
          when_value_is_undefined: "skip"
          otherwise_emit:
            path: "A_Path.PATH_03_keysToPath({keys})"
            value: "value"
          root_leaf_path: "'' (keysToPath([]) === '')"
        array_rules:
          iterate: "i = 0..(length-1)"
          hole: "if !(i in arr) -> skip (no recurse, no emit)"
          key_segment: "String(i)"
        object_rules:
          keys_source: "Object.keys(obj)"
          key_segment: "String(k)"
        guarantees:
          array_holes_skipped: true
          undefined_leaf_skipped: true

      FLAT_01_fromObjToFlat:
        name: "fromObjToFlat"
        in:
          obj: "any"
        out: "Object<string, any>"
        algorithm:
          pairs: "FLAT_03_toFlatPairs(obj)"
          reduce_to_object:
            step: "acc[p.path] = p.value"
            overwrite: "last write wins"
        aliases:
          flatten: "FLAT_01_fromObjToFlat"
        open_Semantic:
          duplicate_paths_in_pairs:
            note: "if duplicates exist, overwrite is last-write-wins (not elevated beyond algorithm)"

      FLAT_04_fromFlatPairs:
        name: "fromFlatPairs"
        in:
          pairs: "any"
        out: "Object|Array"
        algorithm:
          xs: "Array.isArray(pairs) ? pairs : []"
          when_empty: "return {}"
          root_infer_first_entry_only:
            firstPath: "String(xs[0]?.path ?? '')"
            firstKeys: "A_Path.PATH_02_pathToKeys({path:firstPath})"
            firstSegOrEmpty: "String(firstKeys[0] ?? '')"
            init: "H_Guard.GUARD_03_ensureContainerForNextKey({nextKey:firstSegOrEmpty})"
          reduce_build_immutable:
            init: "init"
            step:
              k: "String(p?.path ?? '')"
              v: "p?.value"
              if_v_is_undefined: "return acc"
              otherwise: "return C_Write.WRITE_01_setByPath({target:acc, path:k, value:v})"
          return: "acc"
        open_Semantic:
          root_inference_conflict:
            note: "root type is inferred from first entry only; later needs do not re-infer"
          path_conflicts:
            note: "final outcome follows WRITE_01_setByPath behavior; no extra conflict rules here"

      FLAT_02_fromFlatToObj:
        name: "fromFlatToObj"
        in:
          flat: "any"
        out: "Object|Array"
        algorithm:
          src: "(flat && typeof flat === 'object') ? flat : {}"
          entries: "Object.keys(src).map(k => ({ path:k, value:src[k] }))"
          return: "FLAT_04_fromFlatPairs(entries)"
        aliases:
          unFlatten: "FLAT_02_fromFlatToObj"
        open_Semantic:
          key_enumeration_order:
            note: "Object.keys ordering follows JS behavior (not elevated beyond algorithm)"

      FLAT_05_flatKeys:
        name: "flatKeys"
        in:
          obj: "any"
        out: "string[]"
        defined_as: "FLAT_03_toFlatPairs(obj).map(p => p.path)"

      FLAT_06_flatValues:
        name: "flatValues"
        in:
          obj: "any"
        out: "any[]"
        defined_as: "FLAT_03_toFlatPairs(obj).map(p => p.value)"

    ordering_guarantees:
      consistent_pair_order_across:
        - "FLAT_03_toFlatPairs"
        - "FLAT_05_flatKeys"
        - "FLAT_06_flatValues"
      dfs_order_definition:
        array: "index ascending (0..length-1), skipping holes"
        object: "Object.keys order"

  E_CloneMerge:
    dependencies:
      - "H_Guard.GUARD_01_isPlainObject"
      - "H_Guard.GUARD_02_isContainer"
      - "H_Guard.GUARD_05_isStructuredClonable"
      - "A_Path.PATH_01_normalizePath (imported but not used in this family code)"
      - "B_Read.READ_01_getByPath (imported but not used in this family code)"
      - "C_Write.WRITE_01_setByPath"

    contract_notes:
      empty_definition: "undefined only"
      failure_policy: "unsupported type -> throw; no implicit fallback"

    methods:

      MERGE_01_cloneJsonValue:
        name: "cloneJsonValue"
        in:
          value: "any"
        out: "any"
        algorithm:
          try: "JSON.parse(JSON.stringify(value))"
          catch: "throw Error('cloneJsonValue: value is not JSON-serializable')"
        guarantees:
          throws_on_non_json_serializable: true
        open_Semantic:
          json_stringify_Semantic:
            note: "exact serialization (e.g., Date->string, functions/symbols dropped, undefined handling) follows JS JSON behavior; not redefined here"

      MERGE_01_2_cloneStructuredValue:
        name: "cloneStructuredValue"
        in:
          value: "any"
        out: "any"
        algorithm:
          init: "seen = new WeakSet()"
          go(v):
            precheck: "if !H_Guard.GUARD_05_isStructuredClonable({value:v}) -> throw Error('cloneStructuredValue: value is not structured-clonable')"
            leaf: "if !H_Guard.GUARD_02_isContainer({value:v}) -> return v"
            cycle:
              if_seen: "if seen.has(v) -> throw Error('cloneStructuredValue: cycle detected')"
              then_add: "seen.add(v)"
            array: "if Array.isArray(v) -> return v.map(x => go(x))"
            object:
              out: "{}"
              keys: "Object.keys(v)"
              fill: "out[k] = go(v[k])"
              return: "out"
          return: "go(value)"
        guarantees:
          deep_clone_for_allowed_structured_values: true
          throws_on_cycle: true
          no_fallback: true
        open_Semantic:
          array_holes:
            note: "array clone uses Array.prototype.map; hole handling follows JS spec (not redefined here)"
          key_space:
            note: "enumeration uses Object.keys only; non-enumerable props ignored (not redefined here)"

      MERGE_04_assignDefined:
        name: "assignDefined"
        in:
          target: "any"
          patch: "any"
        out: "Object"
        algorithm:
          out_init:
            when_target_plain_object: "out = { ...target }"
            otherwise: "out = {}"
          src_init:
            when_patch_plain_object: "src = patch"
            otherwise: "src = {}"
          for_each_key_in_Object_keys(src):
            when_value_is_not_undefined: "out[k] = src[k]"
            when_value_is_undefined: "skip"
          return: "out"
        guarantees:
          writes_only_defined: true
          returns_plain_object: true

      MERGE_05_omitUndefined:
        name: "omitUndefined"
        in:
          value: "any"
        out: "any"
        algorithm:
          leaf: "if !H_Guard.GUARD_02_isContainer({value}) -> return value"
          array:
            map: "value.map(v => omitUndefined(v))"
            filter: "keep items where item !== undefined"
            return: "filtered array"
          object:
            out: "{}"
            keys: "Object.keys(value)"
            recurse: "vv = omitUndefined(value[k])"
            keep_rule: "if vv !== undefined -> out[k] = vv"
            return: "out"
        guarantees:
          removes_undefined_recursively: true
        open_Semantic:
          array_holes:
            note: "map/filter behavior on holes follows JS spec; not redefined here"

      MERGE_02_mergeDeep:
        name: "mergeDeep"
        in:
          target: "any"
          source: "any"
          options: "Object? (default {})"
        out: "any"
        options:
          array:
            allowed: ["replace", "concat"]
            default: "replace"
        algorithm:
          arrayStrategy: "options.array || 'replace'"
          non_container_shortcut:
            when_target_not_container_or_source_not_container: "return source"
          both_arrays:
            when_arrayStrategy_concat: "return target.concat(source)"
            otherwise_replace: "return source.slice()"
          container_type_mismatch:
            rule: "if Array.isArray(target) !== Array.isArray(source) -> source wins as whole"
            return:
              when_source_array: "source.slice()"
              when_source_object: "{ ...source }"
          both_plain_objects:
            keys_union: "Set(Object.keys(target) ∪ Object.keys(source))"
            for_each_key_k:
              tv: "target[k]"
              sv: "source[k]"
              if_sv_is_undefined: "out[k] = tv"
              else: "out[k] = mergeDeep({ target: tv, source: sv, options })"
            return: "out"
        guarantees:
          immutable: true
          source_wins_on_type_mismatch: true
          source_undefined_means_keep_target: true
        open_Semantic:
          "plain object vs other object":
            note: "container check is via GUARD_02_isContainer; non-plain objects are treated as leaf and replaced by source"

      MERGE_03_mergeByPaths:
        name: "mergeByPaths"
        in:
          target: "any"
          flat: "any (expected Object<path,value>)"
        out: "any"
        algorithm:
          src: "(flat && typeof flat === 'object') ? flat : {}"
          out: "target"
          for_each_path_in_Object_keys(src):
            v: "src[p]"
            if_v_is_undefined: "skip"
            else: "out = C_Write.WRITE_01_setByPath({ target: out, path: p, value: v })"
          return: "out"
        guarantees:
          writes_only_defined: true
          immutable_via_setByPath: true

  F_DiffPatch:
    dependencies:
      - "D_Flat.FLAT_01_fromObjToFlat"
      - "C_Write.WRITE_01_setByPath"
      - "C_Write.WRITE_05_unsetByPath"

    contract_notes:
      primary_language: "flat path"
      empty_definition: "undefined only (absence)"
      leaf_equality: "Object.is"
      patch_ops:
        allowed: ["set", "unset"]
        forbid_set_undefined: true

    internals:
      isSameLeaf:
        in: { a: "any", b: "any" }
        out: "boolean"
        rule: "Object.is(a, b)"

      diffFlat:
        in:
          fa: "any (expected flat map)"
          fb: "any (expected flat map)"
        out:
          added: "Object<path, any>"
          modified: "Object<path, any>"
          removed: "Object<path, any>"
          unchanged: "Object<path, any>"
        algorithm:
          a: "(fa && typeof fa === 'object') ? fa : {}"
          b: "(fb && typeof fb === 'object') ? fb : {}"
          keys: "new Set([...Object.keys(a), ...Object.keys(b)])"
          for_each_k:
            av: "a[k]"
            bv: "b[k]"
            aHas: "av !== undefined"
            bHas: "bv !== undefined"
            branches:
              - when: "!aHas && bHas"
                do: "added[k] = bv"
              - when: "aHas && !bHas"
                do: "removed[k] = av"
              - when: "aHas && bHas"
                do:
                  same: "Object.is(av, bv)"
                  if_same: "unchanged[k] = bv"
                  else: "modified[k] = bv"
        open_Semantic:
          manual_undefined_in_flat_map:
            note: "existence uses (v !== undefined); if caller passes flat map containing undefined values, behavior follows this rule"

    methods:

      DIFF_01_objDiff:
        name: "objDiff"
        in:
          a: "any"
          b: "any"
        out:
          added: "Object<path, any>"
          modified: "Object<path, any>"
          removed: "Object<path, any>"
          unchanged: "Object<path, any>"
        algorithm:
          fa: "D_Flat.FLAT_01_fromObjToFlat(a)"
          fb: "D_Flat.FLAT_01_fromObjToFlat(b)"
          return: "diffFlat(fa, fb)"

      DIFF_04_changedPaths:
        name: "changedPaths"
        in:
          a: "any"
          b: "any"
        out: "string[]"
        algorithm:
          d: "DIFF_01_objDiff(a, b)"
          paths: "[...Object.keys(d.added), ...Object.keys(d.modified), ...Object.keys(d.removed)]"
          return: "paths.sort()"

      DIFF_05_pickChanged:
        name: "pickChanged"
        in:
          a: "any"
          b: "any"
        out:
          added: "Object<path, any>"
          modified: "Object<path, any>"
          removed: "Object<path, any>"
        algorithm:
          d: "DIFF_01_objDiff(a, b)"
          return: "{ added: d.added, modified: d.modified, removed: d.removed }"

      DIFF_02_diffToPatch:
        name: "diffToPatch"
        in:
          a: "any"
          b: "any"
        out: "Array<{ op:'set'|'unset', path:string, value?:any }>"
        ordering_guarantees:
          - "all unset ops come before all set ops"
          - "unset sorted deep->shallow by string length, tie by lexicographic asc"
          - "set sorted shallow->deep by string length, tie by lexicographic asc"
        algorithm:
          d: "DIFF_01_objDiff(a, b)"
          unsetOps:
            build:
              paths: "Object.keys(d.removed)"
              sort: "desc by path.length; tie lexicographic asc"
              map: "{ op:'unset', path }"
          setOps:
            build:
              paths: "[...Object.keys(d.added), ...Object.keys(d.modified)]"
              sort: "asc by path.length; tie lexicographic asc"
              map:
                op: "'set'"
                path: "path"
                value: "(d.added[path] !== undefined ? d.added[path] : d.modified[path])"
              filter: "value !== undefined (forbid set(undefined))"
          return: "unsetOps.concat(setOps)"
        open_Semantic:
          depth_measure:
            note: "deep/shallow compares by string length, not segment depth"

      DIFF_03_applyPatch:
        name: "applyPatch"
        in:
          target: "any"
          ops: "any"
        out: "any"
        algorithm:
          xs: "Array.isArray(ops) ? ops : []"
          reduce_over_xs:
            read:
              type: "op?.op"
              path: "op?.path"
            branches:
              - when: "type === 'unset'"
                do: "acc = C_Write.WRITE_05_unsetByPath({ target: acc, path })"
              - when: "type === 'set'"
                do:
                  value: "op?.value"
                  if_value_is_undefined: "no-op (return acc)"
                  else: "acc = C_Write.WRITE_01_setByPath({ target: acc, path, value })"
              - when: "otherwise"
                do: "no-op (ignore unknown op)"
          return: "acc"
        guarantees:
          unknown_ops_ignored: true
          immutable_reduce: true

  G_QueryTraverse:
    dependencies:
      - "H_Guard.GUARD_02_isContainer"
      - "A_Path.PATH_03_keysToPath"

    shared_traversal_rules:
      recursion_only_on: "container (plain object / array)"
      leaf_definition: "non-container"
      path:
        build: "keysToPath({ keys })"
        root: { keys: [], path: "" }
      array_children:
        order: "index asc (0..length-1)"
        holes: "if !(i in arr) => skip (no visit, no descent)"
        key_segment: "String(i)"
      object_children:
        order: "Object.keys(obj)"
        key_segment: "String(k)"

    internals:
      listChildren:
        in: { value: "any" }
        out: "Array<{ key:string, child:any }>"
        rules:
          - "if not isContainer(value) => []"
          - "if array: iterate i=0..len-1; if !(i in arr) skip; push { key:String(i), child: arr[i] }"
          - "if plain object: Object.keys(value).map(k => ({ key:String(k), child:value[k] }))"

    methods:

      QUERY_01_walk:
        name: "walk"
        in:
          obj: "any"
          visitor: "(node:Object)=>void"
          options: { mode: "'DFS'|'BFS' (default 'DFS')" }
        out: "void"
        behavior:
          visit_nodes: "visitor called exactly once per node (container + leaf)"
          visit_before_children: true
          mode:
            DFS:
              structure: "stack"
              pop: true
              push_children: true
            BFS:
              structure: "queue"
              shift: true
              push_children: true
          node_shape:
            path: "string"
            keys: "string[]"
            value: "any"
            parent: "any|undefined"
            key: "string|undefined"
            depth: "number"
            isLeaf: "boolean"
            isContainer: "boolean"

      QUERY_01_2_walkSafe:
        name: "walkSafe"
        in:
          obj: "any"
          visitor: "(node:Object)=>void"
          options:
            mode: "'DFS'|'BFS' (default 'DFS')"
            detectCycle: "boolean (default true)"
            onCycle: "'skip'|'throw' (default 'skip')"
        out: "void"
        cycle_detection:
          tracking:
            enabled_when: "detectCycle !== false"
            structure: "WeakSet"
            nodes_tracked: "container nodes where value != null && typeof value === 'object'"
          isCycle:
            definition: "canTrack && seen.has(value)"
          on_cycle:
            throw:
              action: "throw Error(`walkSafe: cycle detected at path=\"${path}\"`)"
              timing: "before visitor call"
            skip:
              action:
                - "still call visitor with isCycle=true"
                - "do not expand children"
          when_not_cycle:
            action:
              - "call visitor with isCycle=false"
              - "if canTrack: seen.add(value)"
              - "expand children as in walk"
        node_shape_additions:
          isCycle: "boolean"

      QUERY_02_mapLeaves:
        name: "mapLeaves"
        in:
          obj: "any"
          fn: "(value:any, meta:Object)=>any"
        out: "any"
        behavior:
          apply_to: "leaf only"
          container_handling: "rebuild container immutably"
          meta:
            path: "string"
            keys: "string[]"
            parent: "any|undefined"
            key: "string|undefined"
            depth: "number"
          arrays:
            holes: "skip (not visited, not preserved)"
            output: "new array built by assigning out[i] for visited indices"
          objects:
            output: "new object with same keys as traversed via Object.keys"

      QUERY_03_filterLeaves:
        name: "filterLeaves"
        in:
          obj: "any"
          fn: "(value:any, meta:Object)=>boolean"
        out: "any"
        behavior:
          apply_to: "leaf only"
          container_not_tested: true
          removal:
            object: "excluded from output (equivalent to delete)"
            array: "kept items pushed => index compressed"
          meta: "same as QUERY_02_mapLeaves meta"
          arrays:
            holes: "skip (not visited, not preserved)"
            output: "new compacted array"
          objects:
            output: "new object with only kept leaves"

      QUERY_04_findByPredicate:
        name: "findByPredicate"
        in:
          obj: "any"
          fn: "(node:Object)=>boolean"
          options: { mode: "'DFS'|'BFS' (default 'DFS')" }
        out: "{ path:string, value:any }|undefined"
        behavior:
          implementation: "uses QUERY_01_walk"
          stop_condition: "first match only (once found, later walk callbacks no-op)"
          return:
            found: "{ path: node.path, value: node.value }"
            not_found: "undefined"
          node_shape: "same as QUERY_01_walk node"

    ordering_guarantees:
      traversal_order_consistency:
        - "walk / walkSafe / findByPredicate share the same traversal order rules for children"
        - "mode selection controls DFS vs BFS"
      children_order:
        array: "index asc"
        object: "Object.keys order"

  I_Convenience:
    principle:
      - "封裝/組合 only"
      - "不新增隱性語義；行為完全由 Read/Write/Flat/Diff 決定"

    dependencies:
      - "B_Read.READ_01_getByPath"
      - "C_Write.WRITE_01_setByPath"
      - "C_Write.WRITE_05_unsetByPath"
      - "D_Flat.FLAT_01_fromObjToFlat"
      - "D_Flat.FLAT_02_fromFlatToObj"
      - "F_Diff.DIFF_01_objDiff"
      - "F_Diff.DIFF_02_diffToPatch"
      - "F_Diff.DIFF_03_applyPatch"
      - "F_Diff.DIFF_04_changedPaths"
      - "F_Diff.DIFF_05_pickChanged"

    methods:

      CONV_01_readWriteFacade:
        name: "readWriteFacade"
        in: { target: "any" }
        out:
          get: "(path:any)=>any"
          set: "({ path:any, value:any })=>any"
          unset: "(path:any)=>any"
        binding_Semantic:
          target: "captured by closure; facade does not update target reference"
        mapping:
          get: "getByPath(target, path)"
          set: "setByPath({ target, path, value })"
          unset: "unsetByPath({ target, path })"
        open_Semantic:
          - "set/unset 回傳 nextTarget，但不回寫到閉包 target；是否提供「持續更新 target」語義未定義"

      CONV_02_flatFacade:
        name: "flatFacade"
        in: { object: "any" }
        out:
          flatten: "()=>Object"
          unflatten: "(flat:Object)=>any"
        binding_Semantic:
          object: "captured by closure"
        mapping:
          flatten: "fromObjToFlat(object)"
          unflatten: "fromFlatToObj(flat)"

      CONV_03_diffFacade:
        name: "diffFacade"
        in: { options: "Object|undefined" }
        out:
          diff: "(a:any,b:any)=>any"
          toPatch: "(a:any,b:any)=>any[]"
          apply: "({ target:any, ops:any[] })=>any"
          applyDiff: "({ target:any, a:any, b:any })=>any"
          changedPaths: "(a:any,b:any)=>string[]"
          pickChanged: "(a:any,b:any)=>any"
        options_Semantic:
          behavior: "currently unused (void options)"
        mapping:
          diff: "objDiff(a, b)"
          toPatch: "diffToPatch(a, b)"
          apply: "applyPatch({ target, ops })"
          applyDiff: "applyPatch({ target, ops: diffToPatch(a, b) })"
          changedPaths: "changedPaths(a, b)"
          pickChanged: "pickChanged(a, b)"
        open_Semantic:
          - "options 欄位與對底層 DIFF 行為的影響未定義（目前等價於未使用）"

      CONV_04_extractByPath:
        name: "extractByPath"
        in:
          path: "any"
          obj: "Object"
        out: "Object"
        behavior:
          src: "(obj && typeof obj === 'object') ? obj : {}"
          for_each_top_key_k: "out[k] = getByPath(src[k], path)"
          preserves_top_level_keys: true
          flatten_unflatten: "not used"
        open_Semantic:
          - "Object.keys 列舉順序依 JS 行為；未提升為承諾"

