Type parameter values and group name collisions by class and namespace home #1118

Open
opened 2026-10-18 19:10:33 +00:00 by jared · 3 comments
Owner

Problem

The name_collision detector finds the wrong collisions. It splits the uses of a parameter name by call shape (path uses versus node uses), not by the class of the value. In run 20261018T172139Z it reports node (OsSdlc::Map::Node and Prism::Node, two separate domains), and it does not report run. The run name is a real collision: six OsSdlc::Crc::*::Run sites receive a Hash that OsSdlc::Boot::Inputs builds from argv, and twelve OsSdlc::CollaboratorPass sites receive an OsSdlc::CollaboratorPass::RunDirectory. The prescriber saw this only by accident, in the rationale of recipes/collaborator_pass.yml of that run.

Approach

Type the values at measure time and group uses by type and home.

1. Value types (ParameterTypes, new). At measure time, CollaboratorPass has the constructor edges with argument text (ConstructorEdge#args, #value_of). ParameterTypes gives a type to a parameter name of a site from the argument text of each edge into that site. The rules, in this order:

  1. X.new(...) or X.for(...): the type is X. Resolve X lexically from the caller namespace to a measured constant, the same way ConstructorGraph::Visitor#resolve does, so Facts and Brief::Facts become one constant. When no measured constant matches, the type is the constant as written (for example Prism::Node). The value is born at the edge line.
  2. A Hash literal {...}: the type is Hash, born at the edge line.
  3. @x: the caller assigns @x = <rule 1 or 2> or @x ||= <rule 1 or 2>. Bare x: the caller assigns the local x that way, or defines a method x whose last expression is such a value (a modifier if and @x ||= are allowed). The value is born at that assignment line. A bare x that is a parameter of the caller's initialize uses rule 4, not this rule.
  4. Pass-through: x or @x that is the caller's own constructor parameter x (or its alias @x = x) gives the callee every type the caller has for x, with the caller's birth. Repeat until no type changes.
  5. Entry seed: for an edge from OsSdlc::Boot::Commands whose argument is a parameter of the script lambda that holds the call, the second lambda parameter is the Input#value of the Inputs method that the entry names (EntryPoints::InputHops already finds that method). Its type is the value: expression of Input.new in that method, by rules 1 to 3, plus one more rule: a merge or compact call on a Hash value is Hash. The value is born at the Input.new line. This gives Hash to the six Crc::*::Run sites.

ParameterTypes#signals emits one signal per site, name, and type: Signal(source: "params", kind: "type", path: <birth path>, line: <birth line>, constant: <site>, names: [name], group: <type>, weight: 1). CollaboratorPass#parameter_signals adds these signals, so they go to signals.tsv with no new run file. A site that gets two types for one name has two type signals.

A use is untyped when its site has no type signal for its name. An untyped use stays out of the grouping. The finding evidence counts the untyped sites. A scratch measure of the current code with rules 1 to 4 typed 87 of 993 use signals (76 of 846 site and name pairs); rule 5 adds the six Crc sites. So about 90% of the uses stay untyped. In the current code node has no typed use, so it leaves the findings for that reason; the namespace rule below is what keeps it out when both types are known.

2. Detector (NameCollisions). Delete SIDES and the path and node split. For each name, join its use signals to the type signals of the same site and name. Group the typed sites by type. The home of a type is:

  • OsSdlc for a constant under OsSdlc::;
  • OsSdlc for a top-level constant with no :: (Ruby core and standard library classes such as Hash, String, Pathname). Such a class has no domain of its own, so it competes with the OsSdlc type for the same name;
  • the first segment for any other namespaced constant (Prism, RuboCop, Reek).

Two types are related when they have the same home. A name is a finding when one home holds two or more of its types. The finding sites are the sorted sites of all types in each such home. Types in other homes are not part of the finding. The evidence is <type> takers <n> for each type in the finding, then untyped <n>, joined with ; . Example for run: Hash takers 6; OsSdlc::CollaboratorPass::RunDirectory takers 12; untyped 2.

UseGroup and the group column of use signals stay as they are; nothing in this ticket reads them.

Shared contract with ticket B. The type signal row has exactly this form: Signal(source: "params", kind: "type", path: <birth path>, line: <birth line>, constant: <site>, names: [name], group: <type>, weight: 1). Ticket B tests against fixture rows of this form, so ticket A and ticket B can be implemented in parallel; neither changes the other's files.

Required work:

  1. Add OsSdlc::CollaboratorPass::ParameterTypes, which emits type signals by the five rules — check: present kind: "type" in plugins/os-sdlc/lib/os_sdlc/collaborator_pass/parameter_types.rb
  2. Add the type signals to the measured signals — check: present ParameterTypes.new in plugins/os-sdlc/lib/os_sdlc/collaborator_pass.rb
  3. Group uses by type and home in NameCollisions — check: absent SIDES in plugins/os-sdlc/lib/os_sdlc/collaborator_pass/name_collisions.rb

Rejected: keep the path and node split and add a type split beside it — two splits give two answers for one name, and the call shape does not tell the class of the value.

Rejected: give a type to an untyped use from the messages sent to it (duck typing) — core types such as Hash answer [] and fetch like many other classes, so the guess is not reliable.

Acceptance criteria

  1. Given use and type signals where run has type Hash at three sites and type OsSdlc::CollaboratorPass::RunDirectory at three other sites, when name collisions are found, then there is one name_collision finding with subject run, all six sites, and evidence Hash takers 3; OsSdlc::CollaboratorPass::RunDirectory takers 3; untyped 0.
  2. Given use and type signals where node has type OsSdlc::Map::Node at some sites and type Prism::Node at the other sites, when name collisions are found, then there is no finding for node.
  3. Given use and type signals where node has type OsSdlc::Map::Node at some sites and type OsSdlc::Brief::Node at the other sites, when name collisions are found, then there is a finding for node.
  4. Given a run collision as in criterion 1 plus one more site that uses run with no type signal, when name collisions are found, then that site is not a finding site and the evidence ends with untyped 1.
  5. Given fixture classes where a command assigns @run = RunDirectory.new(dir) and passes run: @run to a class A, and A passes run: @run to a class B, when parameter types are measured, then A and B each have a run type signal for RunDirectory born at the assignment line.
  6. Given a fixture commands table whose script lambda passes its second parameter as run to a class, and whose Inputs method builds value: with merge on a Hash literal, when parameter types are measured, then that class has a run type signal for Hash born at the Input.new line.
  7. Given a fixture argument Prism::Node.new, when parameter types are measured, then the callee has a type signal for Prism::Node.
  8. Given the os-sdlc code at the commit of this ticket, when bin/collaborator-pass measure runs, then findings.yml has a name_collision finding with subject run and no name_collision finding with subject node.

Owner sketch

class OsSdlc::CollaboratorPass::ParameterTypes
  def initialize(sources, edges, entries)
  def signals
end

class OsSdlc::CollaboratorPass::ParameterTypes::ArgumentType
  def initialize(text, caller_tree, sources)
  def type
  def born_at
end

class OsSdlc::CollaboratorPass::ParameterTypes::EntrySeeds
  def initialize(sources, entries)
  def types
end

class OsSdlc::CollaboratorPass::NameCollisions
  def initialize(signals)
  def findings
end

class OsSdlc::CollaboratorPass::NameCollisions::Home
  def initialize(type)
  def name
end

Non-goals

  • The rename of run to arguments in the Crc sites. The loop prescribes it after this ticket ships.
  • #1117 (RunPath built in the initializer).
  • Removal of UseGroup and of the path and node classification of use signals.
  • Types for uses that the five rules cannot reach, for example values from a block, an OptionParser result, or a method parameter other than initialize.
  • The sharpening step in the rename recipe is ticket B.

Retired tests

plugins/os-sdlc/tests/collaborator_pass/name_collisions_test.rb

Origin

  • Trigger: collaborator-pass iteration 4 run 20261018T172139Z
  • Improvised this session: none
  • Chain: node reported and run not reported (docs/crc/os-sdlc/collaborator-runs/20261018T172139Z/findings.yml, name_collision node, evidence path takers 2; node takers 3) ← NameCollisions compares the path and node sides only (SIDES in plugins/os-sdlc/lib/os_sdlc/collaborator_pass/name_collisions.rb) ← UseGroup classifies a use by call shape, not by the class of the value (plugins/os-sdlc/lib/os_sdlc/collaborator_pass/parameter_uses/use_group.rb) ← DESIGN (plugins/os-sdlc/lib/os_sdlc/collaborator_pass/name_collisions.rb). The run meanings were seen by the prescriber in docs/crc/os-sdlc/collaborator-runs/20261018T172139Z/recipes/collaborator_pass.yml (rationale), and Jared ruled the detector and sharpening design on 2026-10-18 in this session.
  • Root candidate: none found; searched name_collision, name collision, rename recipe, sharpen in open issues
  • Where: plugins/os-sdlc/lib/os_sdlc/collaborator_pass/name_collisions.rb, plugins/os-sdlc/lib/os_sdlc/collaborator_pass/parameter_uses/use_group.rb
  • Session: a71a2c2a-4943-47c4-8dd1-cc16fe42d120
  • Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os/a71a2c2a-4943-47c4-8dd1-cc16fe42d120.jsonl
## Problem The `name_collision` detector finds the wrong collisions. It splits the uses of a parameter name by call shape (`path` uses versus `node` uses), not by the class of the value. In run 20261018T172139Z it reports `node` (`OsSdlc::Map::Node` and `Prism::Node`, two separate domains), and it does not report `run`. The `run` name is a real collision: six `OsSdlc::Crc::*::Run` sites receive a `Hash` that `OsSdlc::Boot::Inputs` builds from argv, and twelve `OsSdlc::CollaboratorPass` sites receive an `OsSdlc::CollaboratorPass::RunDirectory`. The prescriber saw this only by accident, in the rationale of `recipes/collaborator_pass.yml` of that run. ## Approach Type the values at measure time and group uses by type and home. **1. Value types (`ParameterTypes`, new).** At measure time, `CollaboratorPass` has the constructor edges with argument text (`ConstructorEdge#args`, `#value_of`). `ParameterTypes` gives a type to a parameter `name` of a site from the argument text of each edge into that site. The rules, in this order: 1. `X.new(...)` or `X.for(...)`: the type is `X`. Resolve `X` lexically from the caller namespace to a measured constant, the same way `ConstructorGraph::Visitor#resolve` does, so `Facts` and `Brief::Facts` become one constant. When no measured constant matches, the type is the constant as written (for example `Prism::Node`). The value is born at the edge line. 2. A Hash literal `{...}`: the type is `Hash`, born at the edge line. 3. `@x`: the caller assigns `@x = <rule 1 or 2>` or `@x ||= <rule 1 or 2>`. Bare `x`: the caller assigns the local `x` that way, or defines a method `x` whose last expression is such a value (a modifier `if` and `@x ||=` are allowed). The value is born at that assignment line. A bare `x` that is a parameter of the caller's `initialize` uses rule 4, not this rule. 4. Pass-through: `x` or `@x` that is the caller's own constructor parameter `x` (or its alias `@x = x`) gives the callee every type the caller has for `x`, with the caller's birth. Repeat until no type changes. 5. Entry seed: for an edge from `OsSdlc::Boot::Commands` whose argument is a parameter of the script lambda that holds the call, the second lambda parameter is the `Input#value` of the `Inputs` method that the entry names (`EntryPoints::InputHops` already finds that method). Its type is the `value:` expression of `Input.new` in that method, by rules 1 to 3, plus one more rule: a `merge` or `compact` call on a `Hash` value is `Hash`. The value is born at the `Input.new` line. This gives `Hash` to the six `Crc::*::Run` sites. `ParameterTypes#signals` emits one signal per site, name, and type: `Signal(source: "params", kind: "type", path: <birth path>, line: <birth line>, constant: <site>, names: [name], group: <type>, weight: 1)`. `CollaboratorPass#parameter_signals` adds these signals, so they go to `signals.tsv` with no new run file. A site that gets two types for one name has two type signals. A use is untyped when its site has no type signal for its name. An untyped use stays out of the grouping. The finding evidence counts the untyped sites. A scratch measure of the current code with rules 1 to 4 typed 87 of 993 `use` signals (76 of 846 site and name pairs); rule 5 adds the six `Crc` sites. So about 90% of the uses stay untyped. In the current code `node` has no typed use, so it leaves the findings for that reason; the namespace rule below is what keeps it out when both types are known. **2. Detector (`NameCollisions`).** Delete `SIDES` and the path and node split. For each name, join its `use` signals to the `type` signals of the same site and name. Group the typed sites by type. The home of a type is: - `OsSdlc` for a constant under `OsSdlc::`; - `OsSdlc` for a top-level constant with no `::` (Ruby core and standard library classes such as `Hash`, `String`, `Pathname`). Such a class has no domain of its own, so it competes with the OsSdlc type for the same name; - the first segment for any other namespaced constant (`Prism`, `RuboCop`, `Reek`). Two types are related when they have the same home. A name is a finding when one home holds two or more of its types. The finding sites are the sorted sites of all types in each such home. Types in other homes are not part of the finding. The evidence is `<type> takers <n>` for each type in the finding, then `untyped <n>`, joined with `; `. Example for `run`: `Hash takers 6; OsSdlc::CollaboratorPass::RunDirectory takers 12; untyped 2`. `UseGroup` and the `group` column of `use` signals stay as they are; nothing in this ticket reads them. **Shared contract with ticket B.** The type signal row has exactly this form: `Signal(source: "params", kind: "type", path: <birth path>, line: <birth line>, constant: <site>, names: [name], group: <type>, weight: 1)`. Ticket B tests against fixture rows of this form, so ticket A and ticket B can be implemented in parallel; neither changes the other's files. Required work: 1. Add `OsSdlc::CollaboratorPass::ParameterTypes`, which emits `type` signals by the five rules — check: present `kind: "type"` in `plugins/os-sdlc/lib/os_sdlc/collaborator_pass/parameter_types.rb` 2. Add the type signals to the measured signals — check: present `ParameterTypes.new` in `plugins/os-sdlc/lib/os_sdlc/collaborator_pass.rb` 3. Group uses by type and home in `NameCollisions` — check: absent `SIDES` in `plugins/os-sdlc/lib/os_sdlc/collaborator_pass/name_collisions.rb` Rejected: keep the path and node split and add a type split beside it — two splits give two answers for one name, and the call shape does not tell the class of the value. Rejected: give a type to an untyped use from the messages sent to it (duck typing) — core types such as `Hash` answer `[]` and `fetch` like many other classes, so the guess is not reliable. ## Acceptance criteria 1. Given use and type signals where `run` has type `Hash` at three sites and type `OsSdlc::CollaboratorPass::RunDirectory` at three other sites, when name collisions are found, then there is one `name_collision` finding with subject `run`, all six sites, and evidence `Hash takers 3; OsSdlc::CollaboratorPass::RunDirectory takers 3; untyped 0`. 2. Given use and type signals where `node` has type `OsSdlc::Map::Node` at some sites and type `Prism::Node` at the other sites, when name collisions are found, then there is no finding for `node`. 3. Given use and type signals where `node` has type `OsSdlc::Map::Node` at some sites and type `OsSdlc::Brief::Node` at the other sites, when name collisions are found, then there is a finding for `node`. 4. Given a `run` collision as in criterion 1 plus one more site that uses `run` with no type signal, when name collisions are found, then that site is not a finding site and the evidence ends with `untyped 1`. 5. Given fixture classes where a command assigns `@run = RunDirectory.new(dir)` and passes `run: @run` to a class A, and A passes `run: @run` to a class B, when parameter types are measured, then A and B each have a `run` type signal for `RunDirectory` born at the assignment line. 6. Given a fixture commands table whose script lambda passes its second parameter as `run` to a class, and whose `Inputs` method builds `value:` with `merge` on a Hash literal, when parameter types are measured, then that class has a `run` type signal for `Hash` born at the `Input.new` line. 7. Given a fixture argument `Prism::Node.new`, when parameter types are measured, then the callee has a type signal for `Prism::Node`. 8. Given the os-sdlc code at the commit of this ticket, when `bin/collaborator-pass measure` runs, then `findings.yml` has a `name_collision` finding with subject `run` and no `name_collision` finding with subject `node`. ## Owner sketch ```ruby class OsSdlc::CollaboratorPass::ParameterTypes def initialize(sources, edges, entries) def signals end class OsSdlc::CollaboratorPass::ParameterTypes::ArgumentType def initialize(text, caller_tree, sources) def type def born_at end class OsSdlc::CollaboratorPass::ParameterTypes::EntrySeeds def initialize(sources, entries) def types end class OsSdlc::CollaboratorPass::NameCollisions def initialize(signals) def findings end class OsSdlc::CollaboratorPass::NameCollisions::Home def initialize(type) def name end ``` ## Non-goals - The rename of `run` to `arguments` in the `Crc` sites. The loop prescribes it after this ticket ships. - #1117 (`RunPath` built in the initializer). - Removal of `UseGroup` and of the path and node classification of `use` signals. - Types for uses that the five rules cannot reach, for example values from a block, an OptionParser result, or a method parameter other than `initialize`. - The sharpening step in the rename recipe is ticket B. ## Retired tests plugins/os-sdlc/tests/collaborator_pass/name_collisions_test.rb ## Origin - Trigger: collaborator-pass iteration 4 run 20261018T172139Z - Improvised this session: none - Chain: `node` reported and `run` not reported (`docs/crc/os-sdlc/collaborator-runs/20261018T172139Z/findings.yml`, `name_collision` `node`, evidence `path takers 2; node takers 3`) ← `NameCollisions` compares the `path` and `node` sides only (`SIDES` in `plugins/os-sdlc/lib/os_sdlc/collaborator_pass/name_collisions.rb`) ← `UseGroup` classifies a use by call shape, not by the class of the value (`plugins/os-sdlc/lib/os_sdlc/collaborator_pass/parameter_uses/use_group.rb`) ← DESIGN (`plugins/os-sdlc/lib/os_sdlc/collaborator_pass/name_collisions.rb`). The `run` meanings were seen by the prescriber in `docs/crc/os-sdlc/collaborator-runs/20261018T172139Z/recipes/collaborator_pass.yml` (rationale), and Jared ruled the detector and sharpening design on 2026-10-18 in this session. - Root candidate: none found; searched `name_collision`, `name collision`, `rename recipe`, `sharpen` in open issues - Where: `plugins/os-sdlc/lib/os_sdlc/collaborator_pass/name_collisions.rb`, `plugins/os-sdlc/lib/os_sdlc/collaborator_pass/parameter_uses/use_group.rb` - Session: a71a2c2a-4943-47c4-8dd1-cc16fe42d120 - Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os/a71a2c2a-4943-47c4-8dd1-cc16fe42d120.jsonl
Author
Owner

Sibling: #1119 (ticket A gathers type signals; ticket B sharpens). Both ship before the collaborator-pass loop returns to the run name collision. They may be implemented in parallel.

Sibling: #1119 (ticket A gathers type signals; ticket B sharpens). Both ship before the collaborator-pass loop returns to the run name collision. They may be implemented in parallel.
Author
Owner

Work started via /os-sdlc:implement on branch ticket-1118 (worktree .claude/worktrees/ticket-1118).

Work started via /os-sdlc:implement on branch ticket-1118 (worktree .claude/worktrees/ticket-1118).
Author
Owner

Resolution

Done: ParameterTypes (Argument, ArgumentType, AssignedValue, Birth, EntrySeeds) emits type signals by the five rules; CollaboratorPass#parameter_signals adds them; NameCollisions groups typed uses by type and Home, SIDES removed, evidence ends with untyped count. Retired name_collisions_test.rb deleted.

Evidence: Branch ticket-1118 commits f84888e7 and 70fb7a5e; implementation 68 suite-check pass (dispatch 640), behavior-verifier pass (639), contract-auditor pass (641)

Follow-ups: none

Needs review: Read the diff d98dbd19..70fb7a5e on branch ticket-1118 before /os:worktree finish. Confirm acceptance criterion 8 by hand: no gate ran the real bin/collaborator-pass measure.

## Resolution **Done:** ParameterTypes (Argument, ArgumentType, AssignedValue, Birth, EntrySeeds) emits type signals by the five rules; CollaboratorPass#parameter_signals adds them; NameCollisions groups typed uses by type and Home, SIDES removed, evidence ends with untyped count. Retired name_collisions_test.rb deleted. **Evidence:** Branch ticket-1118 commits f84888e7 and 70fb7a5e; implementation 68 suite-check pass (dispatch 640), behavior-verifier pass (639), contract-auditor pass (641) **Follow-ups:** none Needs review: Read the diff d98dbd19..70fb7a5e on branch ticket-1118 before /os:worktree finish. Confirm acceptance criterion 8 by hand: no gate ran the real bin/collaborator-pass measure.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
jared/cc-os#1118
No description provided.