Skip to content

refactor: Decompose program-lib.sh into portable modules (#35) - #50

Open
rubambiza wants to merge 14 commits into
rossoctl:mainfrom
rubambiza:feat/program-lib-decomposition
Open

refactor: Decompose program-lib.sh into portable modules (#35)#50
rubambiza wants to merge 14 commits into
rossoctl:mainfrom
rubambiza:feat/program-lib-decomposition

Conversation

@rubambiza

@rubambiza rubambiza commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Summary

Decompose the ~985-line scripts/program-lib.sh shell library into four flat, self-contained modules behind an unchanged aggregator entrypoint. Pure refactor — no behavior change.

Module Responsibility
core.sh Workspace/temp, portable date math, JSON-schema validation, scan diffing, report I/O
github-api.sh Rate-limit-aware gh wrapper, issue read/close/PR-check
fork.sh Fork/PR creation, issue-field validation, link-fix candidate scoring
org.sh Org-profile loading, core-repo allowlist, canonical-name remap, repos-dir validation
program-lib.sh Thin aggregator sourcing the four modules — the stable entrypoint

Each module has a load-once guard and self-sources only the dependencies it actually references (verified: github-api.sh, fork.sh, and org.sh reference no other module, so they are guard-only; only the aggregator sources all four). The 9 consumer scripts keep source "$SCRIPT_DIR/program-lib.sh" unchanged.

Why

The monolith mixes unrelated concerns and forces wholesale copying when the scripts are vendored per-skill (the Agent Spec does not support referencing external scripts). Focused modules are easier to read, review, and vendor as a subset.

Also in this PR

  • Portability fix: link-health-scanner.sh used declare -A ISSUE_COUNTS (bash 4), which fails on macOS's default bash 3.2. Replaced with a newline-delimited repo<TAB>count accumulator. No output change.
  • CI: new .github/workflows/tests.yml runs the test suite on ubuntu-latest + macos-latest (the macOS leg exercises BSD coreutils and the bash-3.2 floor) and shellchecks the library modules.
  • Docs: README section describing the module layout and the bash-3.2+ portability contract.

Equivalence verification

  • Function-inventory guard (tests/test-lib-inventory.sh): the 23-function public surface is unchanged — none lost or renamed.
  • Per-module standalone smoke test (tests/test-lib-modules.sh): each module sources on its own with its functions defined.
  • Full existing suite green (org-profile, core-repos, pr-review-impact, pr-review-integration with ORG=rossoctl, extract-broken-links, parse-diff-map).
  • Function bodies moved verbatim; the only non-verbatim change is splitting an unparseable inline # shellcheck disable=SC2086 -- comment on canonical_repo_for_dir into a comment line plus a clean directive (same effect).

Notes

Fixes #35

Assisted-By: Claude Code

Design for splitting the ~985-line program-lib.sh into four balanced,
self-contained, bash-3.2-safe modules (core/github-api/fork/org) behind
an unchanged aggregator entrypoint, plus a portability contract and a
Linux+macOS CI matrix. Pure refactor; positioned so the agent-skills
vendor copies (#2149) get clean modular files.

Module boundaries are data-driven (function-usage audit across the 9
consumers); flat layout matches how agent-skills already vendors scripts;
self-sourcing modules with load-once guards keep subset-copying safe.

Assisted-By: Claude Code (Anthropic AI) <noreply@anthropic.com>

Signed-off-by: Gloire Rubambiza <gloire@ibm.com>
Captures the 23-function public surface of program-lib.sh so the
decomposition refactor (rossoctl#35) can prove no function is lost or renamed
as functions move into modules.

Assisted-By: Claude Code (Anthropic AI) <noreply@anthropic.com>

Signed-off-by: Gloire Rubambiza <gloire@ibm.com>
Move workspace/date/json/diff/report helpers into scripts/core.sh with
a load-once guard; program-lib.sh sources it. Verbatim move, no behavior
change; inventory test green.

Assisted-By: Claude Code (Anthropic AI) <noreply@anthropic.com>

Signed-off-by: Gloire Rubambiza <gloire@ibm.com>
Move the gh backoff wrapper and issue read/close/pr-check helpers into
scripts/github-api.sh with a load-once guard. Verbatim move, no behavior
change; inventory test green.

Assisted-By: Claude Code (Anthropic AI) <noreply@anthropic.com>

Signed-off-by: Gloire Rubambiza <gloire@ibm.com>
Move fork/PR creation, issue-field validation, and candidate scoring
into scripts/fork.sh with a load-once guard. These helpers reference no
other module (gh is called directly), so fork.sh is guard-only. Verbatim
move, no behavior change; inventory test green.

Assisted-By: Claude Code (Anthropic AI) <noreply@anthropic.com>

Signed-off-by: Gloire Rubambiza <gloire@ibm.com>
Move org-identity + core-repo helpers into scripts/org.sh (guard-only,
no cross-module deps). program-lib.sh is now a thin aggregator sourcing
the four modules; the entrypoint the 9 consumers use is unchanged. Also
splits an unparseable inline shellcheck directive on canonical_repo_for_dir
into a comment line plus a clean directive (same effect). Verbatim move,
no behavior change; inventory test + full suite green.

Assisted-By: Claude Code (Anthropic AI) <noreply@anthropic.com>

Signed-off-by: Gloire Rubambiza <gloire@ibm.com>
Proves each library module sources on its own (self-sourcing guards +
relative dep resolution) and defines its functions -- the property that
makes a module safe to vendor as a subset.

Assisted-By: Claude Code (Anthropic AI) <noreply@anthropic.com>

Signed-off-by: Gloire Rubambiza <gloire@ibm.com>
link-health-scanner used an associative array (ISSUE_COUNTS), which
fails on macOS's default bash 3.2. Replace with a newline-delimited
repo<TAB>count accumulator plus _ic_get/_ic_incr helpers. Verified
count parity (3/1/0) and no bash-4 constructs remain; no output change.

Assisted-By: Claude Code (Anthropic AI) <noreply@anthropic.com>

Signed-off-by: Gloire Rubambiza <gloire@ibm.com>
Add a test workflow with an ubuntu-latest + macos-latest matrix. The
macOS leg exercises BSD coreutils and the bash-3.2 floor, enforcing the
portability contract (catches declare -A / mapfile / GNU-only flags).
shellcheck targets the five library modules at warning severity; broader
consumer-script linting is tracked as a separate follow-up. Pins
actions/checkout to a SHA with least-privilege permissions.

Assisted-By: Claude Code (Anthropic AI) <noreply@anthropic.com>

Signed-off-by: Gloire Rubambiza <gloire@ibm.com>
Add a README section describing the four library modules, the aggregator
entrypoint, and the bash-3.2+ portability contract enforced by CI. Update
the shared-helpers note to point at the module split.

Assisted-By: Claude Code (Anthropic AI) <noreply@anthropic.com>

Signed-off-by: Gloire Rubambiza <gloire@ibm.com>
@rubambiza rubambiza added enhancement New feature or request ready-for-ai-review Request automated AI code review from clawgenti labels Aug 10, 2026

@clawgenti clawgenti left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Decomposes program-lib.sh into four focused, vendorable modules (core.sh, github-api.sh, fork.sh, org.sh) behind a thin aggregator, fixes the declare -A bash-4 portability blocker in link-health-scanner.sh, and adds a matrix CI workflow (Linux + macOS) with a function-inventory equivalence test. The design is well-executed with load-once guards, correct $BASH_SOURCE-relative dep resolution, and thorough inline documentation.

Finding: fork.sh is missing the self-sourcing of core.sh and github-api.sh specified in the design document (§ "Dependency loading", lines 89–97), which shows those . source lines as part of the module header. The current implementation works because fork.sh’s functions only invoke gh/git directly rather than calling gh_with_backoff or other library helpers — but the discrepancy between spec and implementation may cause confusion for anyone vendoring fork.sh under the assumption it is self-contained with respect to its stated deps. Either add the source lines (matching the spec) or update the design doc to reflect that fork.sh intentionally has no intra-library dependencies.


Reviewed by clawgenti using the github-pr-review skill

Comment thread scripts/fork.sh
# ## Portability
# Targets bash 3.2+ (macOS default) through modern bash.
[ -n "${_FORK_SH_LOADED:-}" ] && return
_FORK_SH_LOADED=1

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The design doc (§ Dependency loading) shows fork.sh sourcing core.sh and github-api.sh here:

_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
. "$_LIB_DIR/core.sh"
. "$_LIB_DIR/github-api.sh"

The current implementation omits these — which is fine because none of fork.sh’s functions actually call gh_with_backoff or other library helpers (they use raw gh/git directly). But it leaves the spec and the code out of sync. Suggest either adding the source lines to match the spec, or adding a # No intra-library deps comment so the omission is intentional and documented for vendoring consumers.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Went with your second suggestion — added a "No intra-library deps" header comment to fork.sh (and to github-api.sh and org.sh for consistency), so the intentional omission is documented right where a vendoring consumer reads it (commit 0df0db5). fork.sh also notes the self-source pattern to add if a function later calls a core.sh/github-api.sh helper. The design doc was corrected in a58ea67 to match. Comment-only; inventory (23 fns) and module smoke tests still pass.

Assisted-By: Claude Code

The design doc §"Dependency loading" showed fork.sh (and by category
github-api.sh, org.sh) self-sourcing core.sh/github-api.sh. The
implementation deliberately deviated: empirical measurement of each
module's function bodies found no cross-module helper calls, so all four
modules are guard-only with no source lines.

Update the spec to reflect the implemented, measured reality — modules
self-source only dependencies they actually reference, and the current
split has none — so a vendored fork.sh is genuinely self-contained and no
module declares an unused dependency. Resolves the PR rossoctl#50 review finding.

Assisted-By: Claude Code (Anthropic AI) <noreply@anthropic.com>

Signed-off-by: Gloire Rubambiza <gloire@ibm.com>

@clawgenti clawgenti left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Solid refactor: clean module split with load-once guards, bash-3.2 portability fix for declare -A, equivalence verified by function-inventory + smoke tests, and CI green on both Linux and macOS.

  • docs/specs/… (latest commit): The CI section says "run shellcheck over scripts/*.sh" but .github/workflows/tests.yml targets only the five library modules (scripts/core.sh scripts/github-api.sh scripts/fork.sh scripts/org.sh scripts/program-lib.sh), not the full glob. Minor doc/code discrepancy — the narrower scope is intentional (consumer-script SC2034 tracked in #52), so the spec should say "library modules" rather than scripts/*.sh.

Reviewed by clawgenti using the github-pr-review skill


- **Matrix:** `ubuntu-latest` + `macos-latest`. The macOS runner exercises BSD `date`/`sed`/`grep`
and an older-bash environment, catching the `declare -A` class of defect automatically.
- **Steps:** run every `tests/*.sh`; run `shellcheck` over `scripts/*.sh`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CI step shellchecks only the five library modules (not scripts/*.sh) — consumer-script linting is intentionally deferred to #52. Consider updating this line to say "library modules" rather than scripts/*.sh to match what the workflow actually does.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e442270 — the spec now says the CI step shellchecks the five library modules (core/github-api/fork/org/program-lib), and explicitly notes consumer-script linting is deferred to #52. Matches what tests.yml runs.

Assisted-By: Claude Code

Add a header comment to fork.sh, github-api.sh, and org.sh stating each
sources no sibling module (functions invoke gh/git/jq/builtins directly),
so a vendoring consumer can copy any one alone. fork.sh also documents the
self-source pattern to add if a real cross-module call is later introduced;
org.sh notes load_org_profile sources a runtime data file, not a module.

Documents the omission in the code itself, where a vendoring consumer reads
it — complementing the spec correction in a58ea67. Comment-only: inventory
(23 fns) and module smoke tests pass, lib shellcheck clean at warning.

Addresses the PR rossoctl#50 inline review thread on scripts/fork.sh.

Assisted-By: Claude Code (Anthropic AI) <noreply@anthropic.com>

Signed-off-by: Gloire Rubambiza <gloire@ibm.com>

@clawgenti clawgenti left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clean, well-scoped refactor: the monolith decomposition is mechanically correct, equivalence guards are solid, and the declare -A portability fix is the right call.

  • docs/specs/…-design.md status field is stale — line 4 reads Design (awaiting review) but the work is fully implemented in this PR; should be updated to Implemented (or similar) so the doc stays trustworthy as a reference.
  • _ic_get/_ic_incr leak into global namespace — the helpers are defined at script top-level in link-health-scanner.sh, not scoped inside a block. The _-prefix reduces collision risk but they will remain in the environment after the scanner exits (relevant if the script is ever sourced rather than executed). Consider a _lhs_ic_get/_lhs_ic_incr prefix or wrapping them in a subshell block if isolation matters.

Reviewed by clawgenti using the github-pr-review skill

# program-lib.sh Decomposition Design — Portable, Vendorable Modules

**Date:** 2026-08-09
**Status:** Design (awaiting review)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Status is stale — this work is implemented in the PR itself. Consider updating to Implemented (or Implemented — 2026-08-09) so the doc remains trustworthy as a historical reference.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e442270 — Status is now "Implemented — 2026-08-09".

Assisted-By: Claude Code

Comment thread scripts/link-health-scanner.sh Outdated
# owner/name (no whitespace), so tab-splitting is unambiguous.
ISSUE_COUNTS="" # rows of "repo<TAB>count"

_ic_get() { # $1=repo -> prints its count, or 0 if absent

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_ic_get (and _ic_incr below) are defined at top level, so they leak into the calling environment if this script is ever sourced. Low risk today since it is executed, but consider a more namespaced prefix (e.g. _lhs_ic_get) to make intent clear.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 738529b — renamed to _lhs_ic_get / _lhs_ic_incr (all 5 sites). Pure token rename, no behavior change; bash -n clean, shellcheck clean on the changed lines, inventory + module + extract-broken-links tests pass.

Assisted-By: Claude Code

Two review-flagged staleness fixes in the design doc:
- Status: "Design (awaiting review)" -> "Implemented — 2026-08-09" (the
  work ships in this PR).
- CI step: the workflow shellchecks the five library modules, not
  scripts/*.sh; consumer-script linting is deferred to rossoctl#52. Spec text now
  says so, matching what tests.yml actually runs.

Addresses two PR rossoctl#50 inline review threads.

Assisted-By: Claude Code (Anthropic AI) <noreply@anthropic.com>

Signed-off-by: Gloire Rubambiza <gloire@ibm.com>
@rubambiza rubambiza self-assigned this Aug 11, 2026
Rename the top-level _ic_get/_ic_incr accumulator helpers to
_lhs_ic_get/_lhs_ic_incr. The script is executed (not sourced) so there
is no live collision, but the namespaced prefix signals intent and avoids
leaking generic names into a callers environment if it is ever sourced.

Pure token rename (all 5 sites), no behavior change: bash -n clean,
shellcheck clean on the changed lines, inventory + module + extract-broken-
links tests pass.

Addresses a PR rossoctl#50 inline review thread on link-health-scanner.sh.

Assisted-By: Claude Code (Anthropic AI) <noreply@anthropic.com>

Signed-off-by: Gloire Rubambiza <gloire@ibm.com>

@clawgenti clawgenti left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clean modular decomposition of program-lib.sh into four focused shell modules behind an unchanged aggregator entrypoint, with equivalence-verified tests and a macOS/bash-3.2 portability fix. All checks pass. Ready for human review.


Reviewed by clawgenti using the github-pr-review skill

@rubambiza rubambiza added ready-for-human-review AI review passed, ready for human reviewer and removed ready-for-ai-review Request automated AI code review from clawgenti labels Aug 11, 2026

@esnible esnible left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Clean shell-script refactor: scripts/program-lib.sh (~985 lines) split into four self-contained modules (core.sh, github-api.sh, fork.sh, org.sh) behind an unchanged aggregator entrypoint. Verified by hand that function bodies moved verbatim (only documented deviations: a bash 3.2 portability fix in link-health-scanner.sh and a shellcheck-comment split), load-once guards are unique per module, and only program-lib.sh sources all four (the other three have zero sibling source statements, confirming the guard-only/self-contained claim). CI's actions/checkout SHA pin was independently verified against upstream — it resolves correctly to the real v4 release. Tests (test-lib-inventory.sh, test-lib-modules.sh) match their claims (23-function inventory unchanged; each module sources standalone). Docs are consistent with the code.

Author: rubambiza (MEMBER — maintainer)
Areas reviewed: Shell, CI/GitHub Actions, Tests, Docs
Agent/IDE config (.claude/.vscode): none
Commits: 14 commits, all signed-off: yes
CI status: passing (DCO, test x2, verify-pr-title)

# Drop any existing row for this repo, then append the new count.
ISSUE_COUNTS=$(printf '%s\n' "$ISSUE_COUNTS" | awk -F'\t' -v r="$1" '$1!=r')
ISSUE_COUNTS="${ISSUE_COUNTS}
$1 $(( cur + 1 ))"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: ISSUE_COUNTS starts as "" and each _lhs_ic_incr append prepends a newline, so the accumulator permanently carries one leading blank line/row. Harmless — _lhs_ic_get's awk match on $1==r never matches a real repo string against the empty field — but slightly untidy if this is ever dumped/debugged directly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request ready-for-human-review AI review passed, ready for human reviewer

Projects

None yet

Development

Successfully merging this pull request may close these issues.

refactor: Decompose program-lib.sh into composable, portable modules

3 participants