=== added file 'rules/annotations/com_origin.rules'
--- old/rules/annotations/com_origin.rules	1970-01-01 00:00:00 +0000
+++ new/rules/annotations/com_origin.rules	2026-08-31 17:04:33 +0000
@@ -0,0 +1,441 @@
+<?xml version="1.0"?>
+
+<!--
+/*
+** Module   : com_origin.rules
+** Abstract : associates every COM/OCX referent with the control or object it originated from
+**
+** Copyright (c) 2026, Golden Code Development Corporation.
+**
+** _#_ _I_ __Date__ _______________________________________Description_______________________________________
+** 001 HC  20260831 Created initial version. Annotates each COM-HANDLE declaration with the identity of the
+**                  control or automation object it was loaded from or assigned from, so that the analytics
+**                  reports can attribute every method and property access to its originating control.
+**                  Refs #11808.
+*/
+ -->
+
+<!--
+** This program is free software: you can redistribute it and/or modify
+** it under the terms of the GNU Affero General Public License as
+** published by the Free Software Foundation, either version 3 of the
+** License, or (at your option) any later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU Affero General Public License for more details.
+**
+** You may find a copy of the GNU Affero GPL version 3 at the following
+** location: https://www.gnu.org/licenses/agpl-3.0.en.html
+**
+** Additional terms under GNU Affero GPL version 3 section 7:
+**
+**   Under Section 7 of the GNU Affero GPL version 3, the following additional
+**   terms apply to the works covered under the License.  These additional terms
+**   are non-permissive additional terms allowed under Section 7 of the GNU
+**   Affero GPL version 3 and may not be removed by you.
+**
+**   0. Attribution Requirement.
+**
+**     You must preserve all legal notices or author attributions in the covered
+**     work or Appropriate Legal Notices displayed by works containing the covered
+**     work.  You may not remove from the covered work any author or developer
+**     credit already included within the covered work.
+**
+**   1. No License To Use Trademarks.
+**
+**     This license does not grant any license or rights to use the trademarks
+**     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
+**     of Golden Code Development Corporation. You are not authorized to use the
+**     name Golden Code, FWD, or the names of any author or contributor, for
+**     publicity purposes without written authorization.
+**
+**   2. No Misrepresentation of Affiliation.
+**
+**     You may not represent yourself as Golden Code Development Corporation or FWD.
+**
+**     You may not represent yourself for publicity purposes as associated with
+**     Golden Code Development Corporation, FWD, or any author or contributor to
+**     the covered work, without written authorization.
+**
+**   3. No Misrepresentation of Source or Origin.
+**
+**     You may not represent the covered work as solely your work.  All modified
+**     versions of the covered work must be marked in a reasonable way to make it
+**     clear that the modified work is not originating from Golden Code Development
+**     Corporation or FWD.  All modified versions must contain the notices of
+**     attribution required in this license.
+-->
+
+<!--
+** Every COM or OCX method/property access in 4GL is made through a COM-HANDLE referent. The
+** referent alone does not say WHICH control or automation object is being driven; that is
+** established elsewhere, at the point the handle was created, loaded or assigned:
+**
+**   CREATE "Excel.Application" chExcel.            COM automation, identity is the ProgID
+**   chFrame = CtrlFrame:COM-HANDLE.                OCX, identity begins with the control frame
+**   chFrame:LoadControls("x.wrx", "Section").      OCX, identity gains the .wrx file and section
+**   chCtl = chFrame:ListView.                      OCX, identity gains the named control
+**   chBook = chExcel:Workbooks:Add().              identity is inherited from the chain root
+**
+** This rule set records that identity on the DECLARATION node of each COM-HANDLE (the node that
+** every reference points at through its "refid" annotation), under two annotations:
+**
+**   com-origin        the control/object identity string
+**   com-origin-kind   com-automation | ocx-control | ocx-control-frame | com-derived
+**
+** A declaration that carries no com-origin annotation after this pass could not be resolved
+** statically (the handle arrives as a parameter, through a shared variable, from a computed
+** ProgID, or from another compilation unit). That is a legitimate and expected outcome; the
+** reports treat a missing annotation as an unresolved origin and count it separately rather
+** than dropping the access.
+**
+** This runs as part of the F2 front end (annotations/early_annotations), NOT the CB back end,
+** because the analytics pipeline only ever executes the front end. The annotations are persisted
+** into the .ast artifacts by BinaryAstCodec and are read back, read-only, by ReportDriver in a
+** separate JVM. Since the report is a later pass over persisted trees, the annotation only has
+** to be correct by the end of this file's walk: origins are therefore written as soon as they
+** are discovered and refined in place as more becomes known, and no deferred application pass
+** is needed.
+-->
+
+<rule-set input="tree">
+
+   <!-- register worker objects -->
+   <worker class="com.goldencode.p2j.uast.ProgressPatternWorker"         namespace="prog" />
+   <worker class="com.goldencode.p2j.convert.ExpressionConversionWorker" namespace="ecw" />
+
+   <!-- node scratch -->
+   <variable name="href"     type="com.goldencode.ast.Aast" />
+   <variable name="enode"    type="com.goldencode.ast.Aast" />
+   <variable name="lref"     type="com.goldencode.ast.Aast" />
+   <variable name="rref"     type="com.goldencode.ast.Aast" />
+   <variable name="cmeth"    type="com.goldencode.ast.Aast" />
+   <variable name="pnode"    type="com.goldencode.ast.Aast" />
+
+   <!-- value scratch -->
+   <variable name="declId"   type="java.lang.Long" />
+   <variable name="srcId"    type="java.lang.Long" />
+   <variable name="ident"    type="java.lang.String" />
+   <variable name="skind"    type="java.lang.String" />
+   <variable name="frame"    type="java.lang.String" />
+   <variable name="wrx"      type="java.lang.String" />
+   <variable name="sect"     type="java.lang.String" />
+
+   <!-- per-file accumulated knowledge, keyed by declaration node id -->
+   <variable name="originOf" type="java.util.Map" />
+   <variable name="kindOf"   type="java.util.Map" />
+   <variable name="frameOf"  type="java.util.Map" />
+   <variable name="wrxOf"    type="java.util.Map" />
+   <variable name="sectOf"   type="java.util.Map" />
+
+   <!-- expression libraries -->
+   <include name="common-progress" />
+
+   <func-library access="private">
+
+      <!-- True only for a genuine COM-HANDLE typed node. The shared com_type() library function
+           also accepts plain widget HANDLE types, which is too broad here: a widget handle is
+           not a COM referent and must not be given a com-origin. -->
+      <function name="com_handle_type">
+         <parameter name="ttype" type="java.lang.Integer" />
+         ttype == prog.var_com_handle  or ttype == prog.field_com_handle   or
+         ttype == prog.func_com_handle or ttype == prog.attr_com_handle    or
+         ttype == prog.meth_com_handle or ttype == prog.oo_meth_com_handle
+      </function>
+
+      <!-- The declaration node id a reference resolves to, or the node's own id when it IS the
+           declaration. Returns null when neither is available. -->
+      <function name="decl_id">
+         <parameter name="target" type="com.goldencode.ast.Aast" />
+         <return    name="rid"    type="java.lang.Long" />
+
+         <rule>rid = null</rule>
+
+         <rule>target != null
+            <rule>target.isAnnotation("refid")
+               <action>rid = #(java.lang.Long) target.getAnnotation("refid")</action>
+               <action on="false">rid = target.getId()</action>
+            </rule>
+         </rule>
+      </function>
+
+      <!-- Descend the left edge of a COM invocation/attribute chain to the referent it is rooted
+           at. For chExcel:Workbooks:Add() this yields the chExcel reference. -->
+      <function name="com_root_ref">
+         <parameter name="target" type="com.goldencode.ast.Aast" />
+         <return    name="ref"    type="com.goldencode.ast.Aast" />
+
+         <rule>ref = target</rule>
+
+         <while>ref != null                            and
+                (ref.type == prog.com_invocation  or
+                 ref.type == prog.colon)
+            <action>ref = ref.getChildAt(0)</action>
+         </while>
+      </function>
+
+      <!-- Literal text of a string node, unquoted and normalized, or null when the node is not a
+           string literal (a computed expression, a variable, a function call). -->
+      <function name="literal_text">
+         <parameter name="target" type="com.goldencode.ast.Aast" />
+         <return    name="txt"    type="java.lang.String" />
+
+         <rule>txt = null</rule>
+
+         <rule>target != null and target.type == prog.string
+            <action>txt = ecw.progressToJavaString(target.text)</action>
+         </rule>
+      </function>
+
+      <!-- The single expression payload under an EXPRESSION wrapper (or the node itself when it
+           is not wrapped). -->
+      <function name="expr_payload">
+         <parameter name="target" type="com.goldencode.ast.Aast" />
+         <return    name="ref"    type="com.goldencode.ast.Aast" />
+
+         <rule>ref = target</rule>
+
+         <while>ref != null and ref.type == prog.expression
+            <action>ref = ref.getChildAt(0)</action>
+         </while>
+      </function>
+
+      <!-- Compose an OCX control identity from the pieces known so far. The .wrx file name is
+           frequently passed to LoadControls() as a variable rather than a literal, so the
+           identity degrades gracefully: wrx#section/control, section/control, or frame. -->
+      <function name="compose_ocx">
+         <parameter name="fname" type="java.lang.String" />
+         <parameter name="wname" type="java.lang.String" />
+         <parameter name="sname" type="java.lang.String" />
+         <parameter name="cname" type="java.lang.String" />
+         <return    name="txt"   type="java.lang.String" />
+
+         <!-- least specific fallback: the control frame's own name -->
+         <rule>txt = fname</rule>
+
+         <rule>sname != null
+            <action>txt = sname</action>
+
+            <rule>wname != null
+               <action>txt = sprintf("%s#%s", wname, sname)</action>
+            </rule>
+         </rule>
+
+         <rule>cname != null
+            <action>txt = sprintf("%s/%s", txt, cname)</action>
+         </rule>
+      </function>
+
+   </func-library>
+
+   <init-rules>
+      <rule>originOf = create("java.util.HashMap")</rule>
+      <rule>kindOf   = create("java.util.HashMap")</rule>
+      <rule>frameOf  = create("java.util.HashMap")</rule>
+      <rule>wrxOf    = create("java.util.HashMap")</rule>
+      <rule>sectOf   = create("java.util.HashMap")</rule>
+   </init-rules>
+
+   <walk-rules>
+
+      <!-- ================================================================================
+           COM automation object creation:  CREATE "Excel.Application" chExcel.
+           Matched on the COM-HANDLE operand rather than on the CREATE_OBJECT node itself, so
+           that no assumption is made about the order of the statement's children.
+           ================================================================================ -->
+      <rule>evalLib("com_handle_type", type)      and
+            parent.type == prog.create_object
+
+         <action>declId = execLib("decl_id", this)</action>
+         <action>enode  = parent.getImmediateChild(prog.expression, null)</action>
+         <action>ident  = execLib("literal_text", execLib("expr_payload", enode))</action>
+
+         <rule>declId != null
+
+            <!-- A non-literal ProgID (CREATE VALUE(expr) h, or a variable) cannot be resolved
+                 statically. Leave the declaration unannotated so it is counted as unresolved
+                 rather than attributed to the wrong object. -->
+            <rule>ident != null
+               <action>originOf.put(declId, ident)</action>
+               <action>kindOf.put(declId, "com-automation")</action>
+               <action>putReferenceNote(declId, "com-origin", ident)</action>
+               <action>putReferenceNote(declId, "com-origin-kind", "com-automation")</action>
+            </rule>
+         </rule>
+      </rule>
+
+      <!-- ================================================================================
+           Control frame binding:  chFrame = CtrlFrame:COM-HANDLE.
+           The COM-HANDLE attribute node's parent chain gives the control frame widget on the
+           left; the enclosing assignment gives the COM-HANDLE variable being bound to it.
+           ================================================================================ -->
+      <rule>type == prog.attr_com_handle             and
+            parent != null                           and
+            (parent.type == prog.colon            or
+             parent.type == prog.com_invocation)
+
+         <action>href  = parent.getChildAt(0)</action>
+         <action>frame = null</action>
+
+         <rule>href != null
+            <action>frame = href.text</action>
+         </rule>
+
+         <!-- locate the assignment target holding this expression -->
+         <action>pnode = parent.parent</action>
+         <action>lref  = null</action>
+
+         <rule>pnode != null                            and
+               (pnode.type == prog.assignment       or
+                pnode.type == prog.assign)
+            <action>lref = pnode.getChildAt(0)</action>
+         </rule>
+
+         <rule>lref != null and frame != null and evalLib("com_handle_type", lref.type)
+            <action>declId = execLib("decl_id", lref)</action>
+
+            <rule>declId != null
+               <action>frameOf.put(declId, frame)</action>
+               <action>ident = execLib("compose_ocx",
+                                       frame,
+                                       #(java.lang.String) wrxOf.get(declId),
+                                       #(java.lang.String) sectOf.get(declId),
+                                       null)</action>
+               <action>originOf.put(declId, ident)</action>
+               <action>kindOf.put(declId, "ocx-control-frame")</action>
+               <action>putReferenceNote(declId, "com-origin", ident)</action>
+               <action>putReferenceNote(declId, "com-origin-kind", "ocx-control-frame")</action>
+            </rule>
+         </rule>
+      </rule>
+
+      <!-- ================================================================================
+           Control loading:  chFrame:LoadControls("controls.wrx", "SectionName").
+           ocx_control_load() matches the second argument of the call, which is the .wrx section
+           name. The first argument is the .wrx file, commonly a variable rather than a literal.
+           ================================================================================ -->
+      <rule>evalLib("ocx_control_load", this)
+
+         <action>cmeth = parent.parent</action>
+         <action>sect  = execLib("literal_text", this)</action>
+         <action>wrx   = null</action>
+         <action>href  = null</action>
+
+         <rule>cmeth != null
+            <!-- first argument: the .wrx file -->
+            <action>pnode = cmeth.getChildAt(0)</action>
+
+            <rule>pnode != null
+               <action>wrx = execLib("literal_text", execLib("expr_payload", pnode.getChildAt(0)))</action>
+            </rule>
+
+            <!-- the referent the call was made on: the control frame's COM-HANDLE -->
+            <rule>cmeth.parent != null and cmeth.parent.type == prog.com_invocation
+               <action>href = execLib("com_root_ref", cmeth.parent.getChildAt(0))</action>
+            </rule>
+         </rule>
+
+         <rule>href != null and evalLib("com_handle_type", href.type)
+            <action>declId = execLib("decl_id", href)</action>
+
+            <rule>declId != null
+               <rule>wrx != null
+                  <action>wrxOf.put(declId, wrx)</action>
+               </rule>
+               <rule>sect != null
+                  <action>sectOf.put(declId, sect)</action>
+               </rule>
+
+               <action>ident = execLib("compose_ocx",
+                                       #(java.lang.String) frameOf.get(declId),
+                                       #(java.lang.String) wrxOf.get(declId),
+                                       #(java.lang.String) sectOf.get(declId),
+                                       null)</action>
+
+               <rule>ident != null
+                  <action>originOf.put(declId, ident)</action>
+                  <action>kindOf.put(declId, "ocx-control-frame")</action>
+                  <action>putReferenceNote(declId, "com-origin", ident)</action>
+                  <action>putReferenceNote(declId, "com-origin-kind", "ocx-control-frame")</action>
+               </rule>
+            </rule>
+         </rule>
+      </rule>
+
+      <!-- ================================================================================
+           Origin inheritance through assignment:
+              chCtl  = chFrame:ListView.            (named OCX control off a control frame)
+              chBook = chExcel:Workbooks:Add().     (object returned from a COM chain)
+              chSheet = chBook:Worksheets(5).       (transitively, from an inherited origin)
+           The assigned handle takes the identity of the referent the chain is rooted at. For a
+           control frame the member name is the control's name and refines the identity; for an
+           automation object the identity is inherited unchanged, since the intermediate objects
+           a chain walks through have no control identity of their own.
+           ================================================================================ -->
+      <rule>(type == prog.assignment or type == prog.assign)   and
+            this.getChildAt(0) != null                         and
+            this.getChildAt(1) != null
+
+         <action>lref = execLib("expr_payload", this.getChildAt(0))</action>
+         <action>rref = execLib("expr_payload", this.getChildAt(1))</action>
+
+         <rule>lref != null                                 and
+               rref != null                                 and
+               evalLib("com_handle_type", lref.type)        and
+               (rref.type == prog.com_invocation       or
+                rref.type == prog.colon)
+
+            <action>href = execLib("com_root_ref", rref)</action>
+
+            <rule>href != null and evalLib("com_handle_type", href.type)
+               <action>srcId  = execLib("decl_id", href)</action>
+               <action>declId = execLib("decl_id", lref)</action>
+
+               <rule>srcId != null and declId != null and declId != srcId
+                  <action>ident = #(java.lang.String) originOf.get(srcId)</action>
+
+                  <action>pnode = rref.getChildAt(1)</action>
+                  <action>skind = #(java.lang.String) kindOf.get(srcId)</action>
+
+                  <rule>ident != null
+
+                     <!-- A control frame yields a NAMED control, so the member name is the
+                          control's name and refines the identity. Stated as two mutually
+                          exclusive sibling rules rather than one rule with an on="false" branch,
+                          because the branch would otherwise have to sit after a nested rule and
+                          its scope becomes ambiguous. -->
+                     <rule>skind != null                          and
+                           skind.equals("ocx-control-frame")     and
+                           pnode != null
+                        <action>ident = execLib("compose_ocx",
+                                                #(java.lang.String) frameOf.get(srcId),
+                                                #(java.lang.String) wrxOf.get(srcId),
+                                                #(java.lang.String) sectOf.get(srcId),
+                                                pnode.text)</action>
+                        <action>originOf.put(declId, ident)</action>
+                        <action>kindOf.put(declId, "ocx-control")</action>
+                        <action>putReferenceNote(declId, "com-origin", ident)</action>
+                        <action>putReferenceNote(declId, "com-origin-kind", "ocx-control")</action>
+                     </rule>
+
+                     <!-- otherwise the origin is inherited verbatim from the chain root, since the
+                          intermediate objects a chain walks through have no identity of their own -->
+                     <rule>skind == null                          or
+                           !skind.equals("ocx-control-frame")    or
+                           pnode == null
+                        <action>originOf.put(declId, ident)</action>
+                        <action>kindOf.put(declId, "com-derived")</action>
+                        <action>putReferenceNote(declId, "com-origin", ident)</action>
+                        <action>putReferenceNote(declId, "com-origin-kind", "com-derived")</action>
+                     </rule>
+                  </rule>
+               </rule>
+            </rule>
+         </rule>
+      </rule>
+
+   </walk-rules>
+
+</rule-set>

=== modified file 'rules/annotations/early_annotations.xml'
--- old/rules/annotations/early_annotations.xml	2026-02-19 12:15:43 +0000
+++ new/rules/annotations/early_annotations.xml	2026-08-31 10:14:50 +0000
@@ -19,6 +19,7 @@
 ** 009 DDF 20250207 Added annotations/early_javanames rule set.
 ** 010 DDF 20250424 Replaced file with artifact.
 ** 011 AOG 20250818 Added multiThreadAccess flag to the global shared variables between conversion threads.
+** 012 HC  20260831 Added annotations/com_origin rule set. Refs #11808.
 */
  -->
  
@@ -222,6 +223,9 @@
    <!-- Annotate abbreviations in tables/fields -->
    <rule-set name="annotations/abbrev_check" />
 
+   <!-- Associate each COM/OCX referent with its originating control or object -->
+   <rule-set name="annotations/com_origin" />
+
    <!-- persist -->
    <rule-set>
       <post-rules>

=== modified file 'rules/include/report.rules'
--- old/rules/include/report.rules	2025-07-02 08:05:30 +0000
+++ new/rules/include/report.rules	2026-08-31 17:14:21 +0000
@@ -63,8 +63,10 @@
 ** 030 OM  20230115          Replaced absolutePath(), relativePath(), upPath() and downPath() with faster
 **                           versions, based on node types.
 ** 031 GES 20241217          Added helper to find all codepage references.
-** 032 PBB 20250702          Fixed the problem with the describe_extent_lvalue function where it would  
+** 032 PBB 20250702          Fixed the problem with the describe_extent_lvalue function where it would
 **                           fail for field types.
+** 033 HC  20260831          Added helpers to attribute COM/OCX method and property accesses to the
+**                           control or object they originate from. Refs #11808.
 */
  -->
  
@@ -906,6 +908,198 @@
          </rule>
       </function>
       
+      <!-- Descend the left edge of a COM invocation chain to the referent it is rooted at. For
+           chExcel:Workbooks:Add() this yields the chExcel reference. Note that get_lvalue() is
+           NOT a substitute: it descends child 1 and therefore lands on a member node rather than
+           on the chain's root referent. -->
+      <function name="com_chain_root">
+         <parameter name="target" type="com.goldencode.ast.Aast" />
+         <return    name="ref"    type="com.goldencode.ast.Aast" />
+
+         <rule>ref = target</rule>
+
+         <while>ref != null                            and
+                (ref.type == prog.com_invocation  or
+                 ref.type == prog.colon)
+            <action>ref = ref.getChildAt(0)</action>
+         </while>
+      </function>
+
+      <!-- For an access made THROUGH a named OCX control hanging off a control frame, the AST node
+           naming that control; null otherwise.
+
+           In real AppBuilder code a named control is never assigned to its own COM-HANDLE; it is
+           reached as a chained property off the frame handle, e.g.
+           chCfTreeView:TreeView:Nodes:Add(...). The control name is therefore the innermost hop of
+           the chain, and it belongs in the control's identity rather than in the member path,
+           because the same .wrx section can expose several controls and a control name on its own
+           is not unique across sections.
+
+           The innermost hop is only a control name when the access reaches through it. When the
+           member IS the innermost hop the access is made on the frame's own COM object (most
+           importantly LoadControls(), but equally a bare control lookup such as chFrame:ListView)
+           and it stays attributed to the frame. -->
+      <function name="com_frame_hop">
+         <variable name="inv"  type="com.goldencode.ast.Aast" />
+         <variable name="ref"  type="com.goldencode.ast.Aast" />
+         <variable name="decl" type="com.goldencode.ast.Aast" />
+         <variable name="kind" type="java.lang.String" />
+         <return   name="hop"  type="com.goldencode.ast.Aast" />
+
+         <rule>hop = null</rule>
+         <rule>inv = parent</rule>
+
+         <!-- descend to the innermost invocation of the chain -->
+         <while>inv.getChildAt(0) != null                       and
+                inv.getChildAt(0).type == prog.com_invocation
+            <action>inv = inv.getChildAt(0)</action>
+         </while>
+
+         <rule>inv.getId() != parent.getId()
+            <action>ref = inv.getChildAt(0)</action>
+
+            <rule>ref != null
+               <action>decl = ref</action>
+
+               <rule>ref.isAnnotation("refid")
+                  <action>decl = getAst(#(long) ref.getAnnotation("refid"))</action>
+               </rule>
+
+               <rule>decl != null and decl.isAnnotation("com-origin-kind")
+                  <action>kind = #(java.lang.String) decl.getAnnotation("com-origin-kind")</action>
+
+                  <rule>kind != null and kind.equals("ocx-control-frame")
+                     <action>hop = inv.getChildAt(1)</action>
+                  </rule>
+               </rule>
+            </rule>
+         </rule>
+      </function>
+
+      <!-- True for a COM/OCX method call or property access. -->
+      <function name="com_member_access">
+         <return name="is_acc" type="java.lang.Boolean" />
+
+         <rule>is_acc = false</rule>
+
+         <rule>parent.type == prog.com_invocation        and
+               (this.type == prog.com_method        or
+                this.type == prog.com_property)
+            <action>is_acc = true</action>
+         </rule>
+      </function>
+
+      <!-- The control or automation object this access is made against, as recorded on the
+           referent's declaration by annotations/com_origin during the F2 front end. Accesses whose
+           origin could not be resolved statically (handle received as a parameter, through a
+           shared variable, from a computed ProgID, or from another compilation unit) are reported
+           as UNRESOLVED rather than dropped. -->
+      <function name="describe_com_control">
+         <variable name="ref"  type="com.goldencode.ast.Aast" />
+         <variable name="decl" type="com.goldencode.ast.Aast" />
+         <variable name="hop"  type="com.goldencode.ast.Aast" />
+         <return   name="txt"  type="java.lang.String" />
+
+         <rule>txt = "UNRESOLVED"</rule>
+
+         <rule>ref = execLib("com_chain_root", parent.getChildAt(0))</rule>
+
+         <rule>ref != null
+            <action>decl = ref</action>
+
+            <rule>ref.isAnnotation("refid")
+               <action>decl = getAst(#(long) ref.getAnnotation("refid"))</action>
+            </rule>
+
+            <rule>decl != null and decl.isAnnotation("com-origin")
+               <action>txt = #(java.lang.String) decl.getAnnotation("com-origin")</action>
+
+               <!-- qualify a control frame's identity with the named control the access is made
+                    through, so the key identifies the control and not just the .wrx section -->
+               <action>hop = execLib("com_frame_hop")</action>
+
+               <rule>hop != null
+                  <action>txt = sprintf("%s/%s", txt, hop.text)</action>
+               </rule>
+            </rule>
+         </rule>
+      </function>
+
+      <!-- The member being accessed, qualified by the chain it is reached through and classified
+           by kind. A method's argument count includes omitted argument slots, since COM treats an
+           omitted argument as a distinct, meaningful case. Examples:
+              Visible [PROPERTY SET]
+              Workbooks [PROPERTY GET]
+              Workbooks:Add() [METHOD, 0 args]
+              Columns:Font:ColorIndex [PROPERTY SET] -->
+      <function name="describe_com_member">
+         <variable name="cur"   type="com.goldencode.ast.Aast" />
+         <variable name="mem"   type="com.goldencode.ast.Aast" />
+         <variable name="hop"   type="com.goldencode.ast.Aast" />
+         <variable name="nargs" type="java.lang.Integer" />
+         <variable name="isset" type="java.lang.Boolean" />
+         <return   name="txt"   type="java.lang.String" />
+
+         <!-- qualify the member with the members of the chain it hangs off of, except for a named
+              OCX control, which identifies the control itself and is carried by the control key -->
+         <rule>txt = text</rule>
+         <rule>hop = execLib("com_frame_hop")</rule>
+         <rule>cur = parent.getChildAt(0)</rule>
+
+         <while>cur != null and cur.type == prog.com_invocation
+            <action>mem = cur.getChildAt(1)</action>
+
+            <rule>mem != null                              and
+                  (hop == null                         or
+                   mem.getId() != hop.getId())
+               <action>txt = sprintf("%s:%s", mem.text, txt)</action>
+            </rule>
+
+            <action>cur = cur.getChildAt(0)</action>
+         </while>
+
+         <rule>this.type == prog.com_method
+
+            <!-- Argument slots, not argument expressions: COM treats an omitted argument as a
+                 distinct and meaningful case, so f(, x) is deliberately reported as 2. The one
+                 exception is an empty argument list, which the grammar still models as a single
+                 empty COM_PARAMETER; f() has no arguments and must report 0. -->
+            <action>nargs = this.getNumImmediateChildren(prog.com_parameter)</action>
+
+            <rule>nargs == 1
+               <action>cur = this.getImmediateChild(prog.com_parameter, null)</action>
+
+               <rule>cur != null and cur.getNumImmediateChildren() == 0
+                  <action>nargs = 0</action>
+               </rule>
+            </rule>
+
+            <action>txt = sprintf("%s() [METHOD, %d args]", txt, nargs)</action>
+         </rule>
+
+         <rule>this.type == prog.com_property
+
+            <!-- A property is written only when ITS OWN invocation is the left hand side of an
+                 assignment. Every inner hop of a chain is a read: in
+                 chExcel:Columns:Font:Bold = TRUE only Bold is written, while Columns and Font are
+                 read on the way to it. Testing the outermost invocation instead would mark every
+                 hop in the chain as a write. -->
+            <action>isset = false</action>
+
+            <rule>parent.parent != null                          and
+                  (parent.parent.type == prog.assign         or
+                   parent.parent.type == prog.assignment)        and
+                  parent.indexPos == 0
+               <action>isset = true</action>
+            </rule>
+
+            <rule>isset
+               <action>txt = sprintf("%s [PROPERTY SET]", txt)</action>
+               <action on="false">txt = sprintf("%s [PROPERTY GET]", txt)</action>
+            </rule>
+         </rule>
+      </function>
+
       <function name="wrx_file_reference">
          <parameter  name="ref"    type="com.goldencode.ast.Aast" />
          <return     name="is_wrx"   type="java.lang.Boolean" />

=== modified file 'rules/reports/profile.rpt'
--- old/rules/reports/profile.rpt	2025-01-09 18:50:12 +0000
+++ new/rules/reports/profile.rpt	2026-08-31 10:17:02 +0000
@@ -74,6 +74,9 @@
 ** 038 GES 20220429          Added reports for class defs with serializable and use-widget-pool.
 ** 039 RFB 20241106          Added reports for abbreviated field names and abbreviated table names.
 ** 040 GES 20241217          Added reports for catch blocks, codepage references, dslog-manager.
+** 041 HC  20260831          Added COM/OCX API usage reports, which attribute every method and
+**                           property access to the control or object it originates from.
+**                           Refs #11808.
 */
  -->
  
@@ -659,7 +662,39 @@
       dumpLevel="1"
       title="OCX Usage by File"
       tags="User Interface,Platform Specific"/>
-   <report 
+   <report
+      condition="evalLib(&quot;com_member_access&quot;)"
+      dumpExpr="execLib(&quot;describe_com_member&quot;)"
+      multiplexExpr="execLib(&quot;describe_com_control&quot;)"
+      title="COM/OCX API Usage by Control"
+      tags="Base Language,Expressions,User Interface,Platform Specific"/>
+   <report
+      condition="lastMatch"
+      dumpExpr="execLib(&quot;describe_com_control&quot;)"
+      multiplexExpr="execLib(&quot;describe_com_member&quot;)"
+      title="COM/OCX API Usage by Member"
+      tags="Base Language,Expressions,User Interface,Platform Specific"/>
+   <report
+      condition="lastMatch"
+      dumpType="parser"
+      dumpLevel="1"
+      multiplexExpr="sprintf(&quot;%s :: %s&quot;, execLib(&quot;describe_com_control&quot;), execLib(&quot;describe_com_member&quot;))"
+      title="COM/OCX API Usage by Control and Member"
+      tags="Base Language,Expressions,User Interface,Platform Specific"/>
+   <report
+      condition="lastMatch"
+      dumpExpr="sprintf(&quot;%s :: %s&quot;, execLib(&quot;describe_com_control&quot;), execLib(&quot;describe_com_member&quot;))"
+      multiplexExpr="file"
+      title="COM/OCX API Usage by File"
+      tags="Base Language,Expressions,User Interface,Platform Specific"/>
+   <report
+      condition="evalLib(&quot;com_member_access&quot;) and execLib(&quot;describe_com_control&quot;).equals(&quot;UNRESOLVED&quot;)"
+      dumpType="parser"
+      dumpLevel="1"
+      multiplexExpr="execLib(&quot;describe_com_member&quot;)"
+      title="COM/OCX Accesses With Unresolved Control Origin"
+      tags="Base Language,Expressions,User Interface,Platform Specific"/>
+   <report
       condition="evalLib(&quot;complex_extent_expr&quot;)"
       dumpType="parser"
       multiplexExpr="execLib(&quot;describe_node&quot;, this, true)"

