Reconcile subcommand mappings using declared subcommands from LLM-extracted pages. For each parent manpage that has a non-empty ``subcommands`` list, find matching child manpages (hyphenated names like ``git-commit``) in the same distro/release and create ``"git commit"`` →
(self)
| 562 | yield row["src"], row["dst"] |
| 563 | |
| 564 | def update_subcommand_mappings(self) -> SubcommandMappingResult: |
| 565 | """Reconcile subcommand mappings using declared subcommands from LLM-extracted pages. |
| 566 | |
| 567 | For each parent manpage that has a non-empty ``subcommands`` list, |
| 568 | find matching child manpages (hyphenated names like ``git-commit``) |
| 569 | in the same distro/release and create ``"git commit"`` → child mappings. |
| 570 | |
| 571 | This is a full reconciliation: all existing subcommand mappings |
| 572 | (``src LIKE '% %'``) are deleted first, then the correct set is |
| 573 | re-inserted. This cleans up stale mappings left by prior runs |
| 574 | (e.g. a parent whose subcommands list shrank after re-extraction). |
| 575 | """ |
| 576 | hyphenated: dict[str, list[str]] = {} # full hyphenated name -> [source, ...] |
| 577 | # name -> [(source, subcommands), ...] — only non-empty subcommands |
| 578 | llm_parents: dict[str, list[tuple[str, list[str]]]] = {} |
| 579 | |
| 580 | rows = self._conn.execute( |
| 581 | "SELECT source, name, subcommands FROM parsed_manpages" |
| 582 | ).fetchall() |
| 583 | for row in rows: |
| 584 | name = row["name"] |
| 585 | source = row["source"] |
| 586 | subcommands = json.loads(row["subcommands"]) |
| 587 | |
| 588 | if subcommands: |
| 589 | llm_parents.setdefault(name, []).append((source, subcommands)) |
| 590 | if "-" in name: |
| 591 | hyphenated.setdefault(name, []).append(source) |
| 592 | |
| 593 | # Compute the full set of valid subcommand mappings. |
| 594 | valid_mappings: set[tuple[str, str]] = set() |
| 595 | parents: dict[str, str] = {} |
| 596 | |
| 597 | for parent_name, parent_entries in llm_parents.items(): |
| 598 | for parent_source, subcommands in parent_entries: |
| 599 | prefix = _dr_prefix(parent_source) |
| 600 | for sub in subcommands: |
| 601 | child_name = f"{parent_name}-{sub}" |
| 602 | for child_source in hyphenated.get(child_name, []): |
| 603 | if not child_source.startswith(prefix): |
| 604 | continue |
| 605 | joined = f"{parent_name} {sub}" |
| 606 | valid_mappings.add((joined, child_source)) |
| 607 | parents[parent_name] = parent_source |
| 608 | |
| 609 | # Delete all existing subcommand mappings and re-insert the valid set |
| 610 | # in a single transaction. Exclude alias mappings where src matches |
| 611 | # the manpage name (e.g. "pg_autoctl activate" whose name has a space). |
| 612 | deleted = self._conn.execute( |
| 613 | "DELETE FROM mappings WHERE src LIKE '% %' " |
| 614 | "AND src NOT IN (SELECT name FROM parsed_manpages WHERE name LIKE '% %')" |
| 615 | ).rowcount |
| 616 | |
| 617 | mappings_to_add = sorted(valid_mappings) |
| 618 | self._conn.executemany( |
| 619 | "INSERT INTO mappings(src, dst, score) VALUES (?, ?, 1)", |
| 620 | [(src, dst) for src, dst in mappings_to_add], |
| 621 | ) |