From c868381d357812f81febfdbb37b020df3166fa89 Mon Sep 17 00:00:00 2001 From: John Zittlau Date: Wed, 10 Jun 2026 14:57:23 -0600 Subject: [PATCH 1/5] Scope libyear-merged deps to the analyzed workspace (LIBTRACK-136) The LIBTRACK-136 fix (3a76c5e) resolved per-workspace current versions, but ~89% of uploaded rows for jobber-frontend still had a blank version. Cause: the "per-workspace" libyear files are the merged union of all workspaces' direct deps (libyear runs `pnpm list --json` + lodash-merge), and parse_libyear mints a standalone library record with an empty current_version for every libyear dep not in the analyzed workspace's resolved set. So each workspace was uploaded ~248 foreign packages with blank versions. Snapshot the resolved dependency set from add_all_libraries BEFORE parse_libyear (it previously snapshotted after, so the libyear union was already "known" and only Dependabot bleed was filtered), then filter parsed_results to that set in filter_to_workspace_packages. Guard the empty-set case so a pnpm list failure does not drop every library. parse_libyear is unchanged; meta_data is computed before filtering and is unaffected. Adds spec/pnpm_spec.rb coverage for: libyear-only deps dropped, in-workspace libyear deps retained + enriched, per-workspace distinctness, and the empty-set guard. Includes OpenSpec change scope-libyear-deps-to-workspace. Co-Authored-By: Amplify 2.1.1 Co-Authored-By: Claude Opus 4.8 --- lib/library_version_analysis/pnpm.rb | 31 +++++++-- .../.openspec.yaml | 2 + .../scope-libyear-deps-to-workspace/design.md | 53 +++++++++++++++ .../proposal.md | 42 ++++++++++++ .../specs/pnpm-version-analysis/spec.md | 25 +++++++ .../scope-libyear-deps-to-workspace/tasks.md | 33 +++++++++ spec/pnpm_spec.rb | 68 +++++++++++++++++++ 7 files changed, 247 insertions(+), 7 deletions(-) create mode 100644 openspec/changes/scope-libyear-deps-to-workspace/.openspec.yaml create mode 100644 openspec/changes/scope-libyear-deps-to-workspace/design.md create mode 100644 openspec/changes/scope-libyear-deps-to-workspace/proposal.md create mode 100644 openspec/changes/scope-libyear-deps-to-workspace/specs/pnpm-version-analysis/spec.md create mode 100644 openspec/changes/scope-libyear-deps-to-workspace/tasks.md diff --git a/lib/library_version_analysis/pnpm.rb b/lib/library_version_analysis/pnpm.rb index 2a44e85..104e2ac 100644 --- a/lib/library_version_analysis/pnpm.rb +++ b/lib/library_version_analysis/pnpm.rb @@ -118,11 +118,16 @@ def get_versions_for_workspace(workspace_path, source) # rubocop:disable Metrics exit(-1) end + # Snapshot the resolved dependency set for THIS workspace BEFORE merging libyear. + # add_all_libraries returns only the analyzed workspace's resolved dependencies, whereas + # libyear (whole-monorepo union) and Dependabot can inject names that belong to other + # workspaces. Filtering back to this set drops that bleed instead of uploading it with a + # blank current_version. (parse_libyear returns all_libraries as the same object, so the + # snapshot must be taken before it adds libyear-only keys.) + workspace_package_names = all_libraries.keys.to_set + puts("\tPNPM [#{source}] parsing libyear") if LibraryVersionAnalysis.dev_output? parsed_results, meta_data = parse_libyear(libyear_results, all_libraries) - # Snapshot before Dependabot: parse_libyear returns all_libraries as parsed_results (same object), - # so any keys Dependabot injects into parsed_results also appear in all_libraries. - workspace_package_names = parsed_results.keys.to_set puts("\tPNPM [#{source}] dependabot") if LibraryVersionAnalysis.dev_output? add_dependabot_findings(parsed_results, meta_data, @github_repo, "pnpm") @@ -155,11 +160,13 @@ def get_versions(source) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength exit(-1) end + # Snapshot the resolved dependency set for this repo BEFORE merging libyear (see the + # workspace variant for the rationale): libyear and Dependabot can inject names beyond the + # resolved set, and parse_libyear mutates all_libraries in place. + workspace_package_names = all_libraries.keys.to_set + puts("\tPNPM parsing libyear") if LibraryVersionAnalysis.dev_output? parsed_results, meta_data = parse_libyear(libyear_results, all_libraries) - # Snapshot before Dependabot: parse_libyear returns all_libraries as parsed_results (same object), - # so any keys Dependabot injects into parsed_results also appear in all_libraries. - workspace_package_names = parsed_results.keys.to_set puts("\tPNPM dependabot") if LibraryVersionAnalysis.dev_output? add_dependabot_findings(parsed_results, meta_data, @github_repo, source) @@ -221,11 +228,21 @@ def add_dependency_graph(parsed_results, workspace_path = nil) # rubocop:disable private + # Restrict parsed_results to the analyzed workspace's resolved dependency set, dropping names + # injected by libyear (whole-monorepo union) or Dependabot that are not actually dependencies + # of this workspace. Those would otherwise upload with a blank current_version. def filter_to_workspace_packages(parsed_results, workspace_package_names, source) + # Guard: if the resolved set is empty (e.g. pnpm list failed and add_all_libraries returned + # {}), do not drop every library. Skip the restriction and keep prior behavior. + if workspace_package_names.empty? + warn "PNPM [#{source}] resolved dependency set is empty; skipping workspace-scope filter" + return + end + injected = parsed_results.keys.reject { |name| workspace_package_names.include?(name) } return if injected.empty? - puts("\tPNPM [#{source}] removing #{injected.count} Dependabot alerts not in this workspace") if LibraryVersionAnalysis.dev_output? + puts("\tPNPM [#{source}] removing #{injected.count} libraries not in this workspace's resolved dependencies") if LibraryVersionAnalysis.dev_output? injected.each { |name| parsed_results.delete(name) } end diff --git a/openspec/changes/scope-libyear-deps-to-workspace/.openspec.yaml b/openspec/changes/scope-libyear-deps-to-workspace/.openspec.yaml new file mode 100644 index 0000000..2cb8041 --- /dev/null +++ b/openspec/changes/scope-libyear-deps-to-workspace/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-06-10 diff --git a/openspec/changes/scope-libyear-deps-to-workspace/design.md b/openspec/changes/scope-libyear-deps-to-workspace/design.md new file mode 100644 index 0000000..4d0d9ce --- /dev/null +++ b/openspec/changes/scope-libyear-deps-to-workspace/design.md @@ -0,0 +1,53 @@ +## Context + +After `3a76c5e`, `Pnpm` resolves current versions per workspace from `pnpm list --depth=0 --json` in `add_all_libraries`, then `parse_libyear` merges libyear latest-version/drift data, then `server_data` uploads `libraries[].version = row.current_version`. + +Two facts about the libyear input make `parse_libyear` produce blank-version, foreign library records: + +1. **libyear carries no installed version.** libyear 0.8.0's JSON per dependency is `{ dependency, drift, pulse, releases, major, minor, patch, available }` — `available` is the latest version; there is no installed/current field. So `parse_libyear` cannot populate `current_version` and never tries; for any dependency not already in `all_libraries` it inserts `new_version_line("")`. +2. **The "per-workspace" libyear files are the whole-monorepo union.** libyear runs `pnpm list --depth=0 --json` and lodash-`merge`s all project objects, so `libyear_.txt` for every workspace contains the union (~248) of all workspaces' direct dependencies. `add_all_libraries`, by contrast, correctly resolves only the analyzed workspace's deps. + +The mismatch: `parse_libyear` injects ~248 names into every workspace; only the handful that intersect that workspace's resolved set carry a current version, and the rest upload blank. + +The existing `filter_to_workspace_packages` does not help, because its "known" snapshot is taken **after** `parse_libyear`: + +```ruby +parsed_results, meta_data = parse_libyear(libyear_results, all_libraries) +workspace_package_names = parsed_results.keys.to_set # already includes the libyear union +add_dependabot_findings(...) +filter_to_workspace_packages(parsed_results, workspace_package_names, source) # only strips Dependabot adds +``` + +## Goals / Non-Goals + +**Goals** +- Stop uploading library records for dependencies that are not in the analyzed workspace's resolved dependency set. +- Eliminate blank `current_version` rows for pnpm repos that arise from libyear-only dependencies. +- Preserve per-workspace current-version resolution, libyear enrichment of in-workspace libraries, the `a..b` range form, and vulnerability/dependency-graph handling. + +**Non-Goals** +- Changing how libyear files are generated in `jobber-frontend` (the "option B" source fix). Considered and deferred — see Decisions. +- Backfilling current versions for transitive or other-workspace dependencies (they should not be uploaded per-workspace as standalone libraries). +- Cleaning existing rows in Library Tracking (separate `library_tracking`-owned change). +- The equivalent `parse_libyear` pattern in `npm.rb` / `gemfile.rb`. + +## Decisions + +**1. Scope the upload to the resolved workspace set (option A), in the gem.** +Snapshot `all_libraries.keys` returned by `add_all_libraries` (the authoritative resolved dependency set for the analyzed workspace) **before** calling `parse_libyear`, then filter `parsed_results` to that set after the libyear merge and Dependabot injection. libyear continues to enrich libraries that are in the set; libyear-only names are dropped instead of uploaded blank. + +Rationale: the resolved set from `pnpm list --json` is already correct and authoritative per workspace (that is exactly what `3a76c5e` fixed). Filtering to it removes both the blank versions and the cross-workspace mis-attribution in one move, entirely within the gem, with no dependency on libyear's generation behavior. + +**2. Implement by extending `filter_to_workspace_packages`, not rewriting `parse_libyear`.** +Move/duplicate the snapshot so it captures the resolved set before `parse_libyear`, and reuse the existing filter to delete out-of-set names. Keeps `parse_libyear` unchanged (it still safely enriches; any entries it adds get filtered out afterward), and unifies libyear-bleed removal with the Dependabot-bleed removal the filter already does. + +**3. Option B (per-workspace libyear at the source) rejected for this change.** +Making `libyear_.txt` genuinely per-workspace would require working around libyear 0.8.0's `pnpm list --json` + `merge(...)` union behavior in `jobber-frontend`, is cross-repo and more invasive, and still would not retroactively fix data. Option A fixes the ticket symptom and the mis-attribution with a smaller, gem-local blast radius. Option B remains a reasonable future data-pipeline cleanup but is not required. + +**4. Empty current versions remain valid only for in-workspace deps.** +A dependency that is in the workspace's resolved set but has no semver (e.g. `link:` / `workspace:` — already skipped by `current_version_from_info`) is governed by existing spec behavior. The new constraint only removes records for names absent from the resolved set; it does not change handling of in-workspace deps. + +## Risks / Trade-offs + +- **Outdated transitive or other-workspace deps are no longer surfaced per workspace.** This is intended: they are not dependencies of the analyzed workspace and were previously uploaded as blank-version noise. Aggregate libyear metrics (`meta_data`) are computed in `parse_libyear` before filtering and are unaffected. +- **Reliance on `add_all_libraries` correctness.** If the resolved set is empty (e.g. `pnpm list` failed and returned `nil`), filtering to it would drop everything. Guard: when the resolved set is empty, skip the libyear-scope filter (fall back to current behavior) and log, rather than uploading an empty library list. diff --git a/openspec/changes/scope-libyear-deps-to-workspace/proposal.md b/openspec/changes/scope-libyear-deps-to-workspace/proposal.md new file mode 100644 index 0000000..10b81a0 --- /dev/null +++ b/openspec/changes/scope-libyear-deps-to-workspace/proposal.md @@ -0,0 +1,42 @@ +## Why + +The LIBTRACK-136 fix (`3a76c5e`, "Fix blank pnpm current versions…") resolved current versions from structured `pnpm list --json`, so each workspace's own direct/dev dependencies now upload real versions. The reporter confirmed it got "better but not fully fixed": ~89% of uploaded rows for `jobber-frontend` (repository 34) still carry a blank version. + +The remaining blanks are a **different defect that the prior fix explicitly scoped out** (its design.md lists "Changing libyear sourcing" as a Non-Goal). `Pnpm#parse_libyear` mints a standalone library record with an empty `current_version` for every dependency in the libyear report that is not in the analyzed workspace's resolved dependency set: + +```ruby +vv = all_libraries[line["dependency"]] +if vv.nil? + vv = new_version_line("") # blank current_version + all_libraries[line["dependency"]] = vv +end +``` + +This is fatal in `jobber-frontend` because the "per-workspace" `libyear_.txt` files are actually the **merged union of every workspace's direct dependencies**: libyear 0.8.0 runs `pnpm list --depth=0 --json` and lodash-`merge`s all project objects together, and its JSON omits the installed version entirely (only `dependency, drift, pulse, releases, major, minor, patch, available`). So every one of the 24 workspaces is uploaded ~248 foreign packages that do not belong to it, each with a blank version. + +Evidence — Jun 10 nightly (post-fix) CSV, repository 34: + +- 6,725 rows; **5,975 (89%) blank version**. +- The blank set is a near-constant ~248 names present in **every** workspace. +- Each name carries a real version only in the 1–3 workspaces where it is genuinely a dependency (e.g. `@fullcalendar/core` → versioned in `apps/jobber-online`, blank in 24 others). + +## What Changes + +- Restrict the libraries carried into the upload to the **analyzed workspace's resolved dependency set**. libyear data SHALL only enrich libraries already resolved from that workspace's `pnpm list --json` output (`add_all_libraries`). libyear-reported dependencies that are absent from the workspace SHALL NOT be added as standalone library records. +- Implementation: snapshot the resolved workspace package names from `add_all_libraries` **before** `parse_libyear` runs, then filter `parsed_results` down to that set — extending the existing `filter_to_workspace_packages` (which today only removes Dependabot-injected names; its snapshot is taken *after* `parse_libyear`, so the libyear bleed is already "known" and survives). +- Preserve everything the prior fix established: per-workspace current-version resolution (`3a76c5e`), libyear latest-version/drift enrichment for in-workspace libraries, the `a..b` multi-version range form, and vulnerability / dependency-graph handling for retained libraries. + +## Capabilities + +### Modified Capabilities + +- `pnpm-version-analysis`: adds the constraint that uploaded libraries are limited to the analyzed workspace's resolved dependencies, so libyear's whole-monorepo report cannot introduce foreign, version-less library records. + +## Impact + +- **Code**: `lib/library_version_analysis/pnpm.rb` — capture the resolved workspace package set before `parse_libyear`; extend `filter_to_workspace_packages` to drop libyear-only entries. Callers `get_versions` (single-package) and `get_versions_for_workspace` (monorepo). +- **Behavior**: per-workspace uploads carry only that workspace's real dependencies, each with a version. Blank-version rows stop being produced. The ~248-foreign-package-per-workspace mis-attribution is eliminated. +- **Tests**: `spec/pnpm_spec.rb` — cover that libyear-only deps are dropped, and that in-workspace deps appearing in libyear are retained and enriched (current + latest version). +- **Prerequisite — new branch**: this change must be built on a branch cut from latest `origin/master`, which includes fix `3a76c5e` and the OpenSpec scaffolding. The prior task worktree branch (`46292689…`) predates both and has no `openspec/` directory. This proposal was authored on `libtrack-136-scope-libyear-deps-to-workspace` (off `origin/master`). +- **Companion change (separately owned)**: a code fix does not retroactively clean rows already in Library Tracking. Existing blank/foreign rows are addressed by the `library_tracking` change `cleanup-blank-library-versions`. +- **Related, out of scope**: the same blank-minting `parse_libyear` pattern exists in `npm.rb` and `gemfile.rb` (other repos/sources). Not changed here; noted for follow-up. diff --git a/openspec/changes/scope-libyear-deps-to-workspace/specs/pnpm-version-analysis/spec.md b/openspec/changes/scope-libyear-deps-to-workspace/specs/pnpm-version-analysis/spec.md new file mode 100644 index 0000000..ffa4d5b --- /dev/null +++ b/openspec/changes/scope-libyear-deps-to-workspace/specs/pnpm-version-analysis/spec.md @@ -0,0 +1,25 @@ +## ADDED Requirements + +### Requirement: Tracked libraries are limited to the analyzed workspace's dependencies + +The system SHALL upload library records only for dependencies present in the analyzed workspace's resolved dependency set (the `dependencies` and `devDependencies` resolved from that workspace's `pnpm list --json`). Dependencies that appear only in the libyear report — including dependencies that belong to other workspaces because the libyear report spans the whole monorepo — SHALL NOT be added as standalone library records. + +#### Scenario: libyear report includes dependencies from other workspaces + +- **WHEN** the libyear report used for workspace `packages/tsconfig` includes `@fullcalendar/core`, which is a dependency of `apps/jobber-online` and not of `packages/tsconfig` +- **THEN** `@fullcalendar/core` SHALL NOT appear as a library record in the `packages/tsconfig` results + +#### Scenario: No blank-version records from libyear-only dependencies + +- **WHEN** the libyear report contains a dependency that has no entry in the analyzed workspace's resolved dependency set +- **THEN** the system SHALL NOT create a library record with an empty current version for that dependency + +#### Scenario: In-workspace dependency present in libyear is retained and enriched + +- **WHEN** a dependency is in both the analyzed workspace's resolved dependency set and the libyear report +- **THEN** its library record SHALL be retained and SHALL carry both the resolved current version and the libyear-provided latest version + +#### Scenario: Resolved set unavailable + +- **WHEN** the analyzed workspace's resolved dependency set cannot be determined (e.g. `pnpm list --json` failed and returned no data) +- **THEN** the system SHALL NOT drop all libraries as out-of-scope, and SHALL instead skip the workspace-scope restriction and log that it was skipped diff --git a/openspec/changes/scope-libyear-deps-to-workspace/tasks.md b/openspec/changes/scope-libyear-deps-to-workspace/tasks.md new file mode 100644 index 0000000..e2aee78 --- /dev/null +++ b/openspec/changes/scope-libyear-deps-to-workspace/tasks.md @@ -0,0 +1,33 @@ +## 0. Prerequisite — branch + +- [x] 0.1 Ensure work is on a branch cut from latest `origin/master` (includes fix `3a76c5e` + OpenSpec scaffolding). This change was authored on `libtrack-136-scope-libyear-deps-to-workspace`; do not implement on the pre-fix task worktree branch (`46292689…`), which has no `openspec/`. + +## 1. Confirm the resolved-set vs libyear-set gap + +- [ ] 1.1 From a `jobber-frontend` worktree, capture `add_all_libraries` output for a small workspace (e.g. `packages/tsconfig`) and that workspace's `libyear_packages-tsconfig.txt`; confirm libyear lists ~248 names while the resolved set lists only that workspace's deps. _(Pending live capture; the union behavior was confirmed from libyear 0.8.0 source + the CSV.)_ +- [x] 1.2 Confirm the blank rows in the CSV correspond exactly to `libyear_names − resolved_names` for each workspace. _(Confirmed during exploration: 5,975 blank rows; a near-constant ~248-name set blank in every workspace, each name versioned only in the 1–3 workspaces where it is a real dependency.)_ + +## 2. Scope the upload to the resolved workspace set + +- [x] 2.1 In `Pnpm#get_versions_for_workspace` and `Pnpm#get_versions`, snapshot the resolved workspace package names from `add_all_libraries` **before** calling `parse_libyear` (the authoritative per-workspace set). +- [x] 2.2 Extend `filter_to_workspace_packages` so the post-merge filter uses that pre-`parse_libyear` snapshot, dropping both libyear-only and Dependabot-only names that are not in the resolved set. +- [x] 2.3 Guard the empty case: if the resolved set is empty (e.g. `pnpm list` failed / returned `nil`), skip the libyear-scope filter and log, rather than uploading an empty library list. +- [x] 2.4 Confirm `parse_libyear` is unchanged and aggregate `meta_data` (computed before filtering) is unaffected. + +## 3. Tests + +- [x] 3.1 In `spec/pnpm_spec.rb`, add coverage: a libyear report containing a name **not** in the resolved set is dropped (no blank-version record). +- [x] 3.2 A name present in **both** the resolved set and libyear is retained and carries current version + libyear `available`/major/minor/patch. +- [x] 3.3 Per-workspace distinctness preserved: a dependency of workspace A does not appear in workspace B's results via libyear. +- [x] 3.4 Empty-resolved-set guard: filter is skipped and analysis does not abort. +- [x] 3.5 `bundle exec rspec` passes. _(120 examples, 0 failures — 116 prior + 4 new.)_ + +## 4. Verify against jobber-frontend + +- [ ] 4.1 Run analysis for ≥2 workspaces and confirm zero blank `current_version` entries in the upload payload (`server_data`) and that each workspace's library set matches its own resolved deps. +- [ ] 4.2 Spot-check that previously-foreign names (e.g. `@fullcalendar/core` in `packages/tsconfig`) are absent from workspaces where they are not dependencies. + +## 5. Release & rollout + +- [ ] 5.1 (Release-time follow-up) Bump the gem version + tag a release, then advance `jobber-frontend` `bin/Gemfile`'s git tag to consume the fix (separate `jobber-frontend`-owned change). +- [ ] 5.2 Coordinate with the `library_tracking` `cleanup-blank-library-versions` change so existing blank/foreign rows are cleaned after the new analyzer has run at least once. diff --git a/spec/pnpm_spec.rb b/spec/pnpm_spec.rb index c93b2c2..8403e15 100644 --- a/spec/pnpm_spec.rb +++ b/spec/pnpm_spec.rb @@ -987,4 +987,72 @@ def do_compare(result:, owner:, current_version:, latest_version:, major:, minor expect(parsed_results["unknown-lib"].owner).to eq(":default") end end + + describe "#get_versions_for_workspace libyear scoping" do + let(:analyzer) { LibraryVersionAnalysis::Pnpm.new("test") } + + def versionline(current) + LibraryVersionAnalysis::Versionline.new(owner: ":unknown", current_version: current) + end + + # libyear reports a name that IS in the resolved set (react) and a foreign one + # (@fullcalendar/core) that belongs to another workspace via the merged-union libyear file. + let(:libyear) do + '[{"dependency":"react","drift":0.3,"major":1,"minor":2,"patch":3,"available":"19.0.0"},' \ + '{"dependency":"@fullcalendar/core","drift":1.7,"major":1,"minor":10,"patch":7,"available":"5.10.1"}]' + end + + before do + allow(analyzer).to receive(:run_libyear_for_workspace).and_return(libyear) + allow(analyzer).to receive(:add_dependabot_findings).and_return(nil) + allow(analyzer).to receive(:add_dependency_graph).and_return({}) + allow(analyzer).to receive(:break_cycles) + allow(analyzer).to receive(:add_ownerships) + end + + it "drops libyear-only deps not in the resolved workspace set" do + allow(analyzer).to receive(:add_all_libraries).with("/project/apps/jobber-online") + .and_return({ "react" => versionline("19.2.3") }) + + parsed, = analyzer.get_versions_for_workspace("/project/apps/jobber-online", "apps/jobber-online") + + expect(parsed).to have_key("react") + expect(parsed).not_to have_key("@fullcalendar/core") + end + + it "retains and enriches a dep present in both the resolved set and libyear" do + allow(analyzer).to receive(:add_all_libraries).with("/project/apps/jobber-online") + .and_return({ "react" => versionline("19.2.3") }) + + parsed, = analyzer.get_versions_for_workspace("/project/apps/jobber-online", "apps/jobber-online") + + expect(parsed["react"].current_version).to eq("19.2.3") + expect(parsed["react"].latest_version).to eq("19.0.0") + expect(parsed["react"].major).to eq(1) + expect(parsed["react"].minor).to eq(2) + expect(parsed["react"].patch).to eq(3) + end + + it "keeps per-workspace results distinct (foreign libyear dep excluded)" do + allow(analyzer).to receive(:add_all_libraries).with("/project/packages/core") + .and_return({ "lodash" => versionline("4.17.21") }) + + parsed, = analyzer.get_versions_for_workspace("/project/packages/core", "packages/core") + + expect(parsed.keys).to contain_exactly("lodash") + expect(parsed).not_to have_key("react") + expect(parsed).not_to have_key("@fullcalendar/core") + end + + it "skips the scope filter (does not drop everything) when the resolved set is empty" do + allow(analyzer).to receive(:add_all_libraries).and_return({}) + allow(analyzer).to receive(:warn) + + parsed, = analyzer.get_versions_for_workspace("/project/apps/jobber-online", "apps/jobber-online") + + # Prior behavior preserved: libyear-only entries are retained rather than wiped. + expect(parsed).to have_key("react") + expect(parsed).to have_key("@fullcalendar/core") + end + end end From 9b271a0b8c1ed72759f01f0704da1ac4550389a4 Mon Sep 17 00:00:00 2001 From: John Zittlau Date: Wed, 10 Jun 2026 16:27:23 -0600 Subject: [PATCH 2/5] Distinguish unresolved vs empty workspace dep sets (LIBTRACK-136) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Local verification against jobber-frontend revealed the empty-set guard was too coarse: workspaces with no registry-versioned direct deps (only link:/workspace: deps, or none — e.g. packages/tsconfig, packages/graphql-depth-limit-plugin, and apps/harbour in a partial install) resolved to {} and the guard SKIPPED the filter, re-emitting the whole ~215-entry libyear union as blank versions. add_all_libraries now returns nil when the resolved set cannot be DETERMINED (pnpm list failed, unparseable output, or no matching workspace entry) and an (possibly empty) hash on success. Callers snapshot resolved&.keys&.to_set, and filter_to_workspace_packages skips only when the snapshot is nil. An empty set still filters, dropping the foreign libyear union instead of uploading blanks. Verified across all 24 jobber-frontend workspaces: 0 blank current_version (was ~89% blank); total libraries 1,270 -> 625. Updated specs and the OpenSpec design/spec-delta to document the nil-vs-empty distinction. rspec: 124 examples, 0 failures. Co-Authored-By: Amplify 2.1.1 Co-Authored-By: Claude Opus 4.8 --- lib/library_version_analysis/pnpm.rb | 40 ++++++----- .../scope-libyear-deps-to-workspace/design.md | 6 +- .../specs/pnpm-version-analysis/spec.md | 11 +++- .../scope-libyear-deps-to-workspace/tasks.md | 6 +- spec/pnpm_spec.rb | 66 +++++++++++++++++-- 5 files changed, 100 insertions(+), 29 deletions(-) diff --git a/lib/library_version_analysis/pnpm.rb b/lib/library_version_analysis/pnpm.rb index 104e2ac..b919a32 100644 --- a/lib/library_version_analysis/pnpm.rb +++ b/lib/library_version_analysis/pnpm.rb @@ -106,9 +106,9 @@ def source_name_for_workspace(workspace_path) # Analyze a single workspace def get_versions_for_workspace(workspace_path, source) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength - all_libraries = {} puts("\tPNPM [#{source}] adding all libraries") if LibraryVersionAnalysis.dev_output? - all_libraries = add_all_libraries(workspace_path) + resolved_libraries = add_all_libraries(workspace_path) + all_libraries = resolved_libraries || {} puts("\tPNPM [#{source}] running libyear") if LibraryVersionAnalysis.dev_output? @@ -123,8 +123,9 @@ def get_versions_for_workspace(workspace_path, source) # rubocop:disable Metrics # libyear (whole-monorepo union) and Dependabot can inject names that belong to other # workspaces. Filtering back to this set drops that bleed instead of uploading it with a # blank current_version. (parse_libyear returns all_libraries as the same object, so the - # snapshot must be taken before it adds libyear-only keys.) - workspace_package_names = all_libraries.keys.to_set + # snapshot must be taken before it adds libyear-only keys.) nil means the resolved set + # could not be determined (pnpm failed) -> the filter is skipped rather than wiping data. + workspace_package_names = resolved_libraries&.keys&.to_set puts("\tPNPM [#{source}] parsing libyear") if LibraryVersionAnalysis.dev_output? parsed_results, meta_data = parse_libyear(libyear_results, all_libraries) @@ -148,9 +149,9 @@ def get_versions_for_workspace(workspace_path, source) # rubocop:disable Metrics end def get_versions(source) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength - all_libraries = {} puts("\tPNPM adding all libraries") if LibraryVersionAnalysis.dev_output? - all_libraries = add_all_libraries + resolved_libraries = add_all_libraries + all_libraries = resolved_libraries || {} puts("\tPNPM running libyear") if LibraryVersionAnalysis.dev_output? @@ -162,8 +163,9 @@ def get_versions(source) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength # Snapshot the resolved dependency set for this repo BEFORE merging libyear (see the # workspace variant for the rationale): libyear and Dependabot can inject names beyond the - # resolved set, and parse_libyear mutates all_libraries in place. - workspace_package_names = all_libraries.keys.to_set + # resolved set, and parse_libyear mutates all_libraries in place. nil means the resolved + # set could not be determined (pnpm failed) -> skip the filter rather than wipe data. + workspace_package_names = resolved_libraries&.keys&.to_set puts("\tPNPM parsing libyear") if LibraryVersionAnalysis.dev_output? parsed_results, meta_data = parse_libyear(libyear_results, all_libraries) @@ -232,10 +234,12 @@ def add_dependency_graph(parsed_results, workspace_path = nil) # rubocop:disable # injected by libyear (whole-monorepo union) or Dependabot that are not actually dependencies # of this workspace. Those would otherwise upload with a blank current_version. def filter_to_workspace_packages(parsed_results, workspace_package_names, source) - # Guard: if the resolved set is empty (e.g. pnpm list failed and add_all_libraries returned - # {}), do not drop every library. Skip the restriction and keep prior behavior. - if workspace_package_names.empty? - warn "PNPM [#{source}] resolved dependency set is empty; skipping workspace-scope filter" + # nil means the resolved set could not be DETERMINED (pnpm list failed). Skip the filter + # so we don't wipe libyear data. An EMPTY set means the workspace was resolved and has no + # registry-versioned direct deps; filter normally so the whole-monorepo libyear union is + # dropped instead of uploaded with blank versions. + if workspace_package_names.nil? + warn "PNPM [#{source}] could not determine resolved dependencies; skipping workspace-scope filter" return end @@ -303,21 +307,27 @@ def run_libyear_open3 # rendered tree output: the text rendering depends on the terminal (e.g. the # `├── ` prefix is absent in non-TTY/CI runs), which previously caused every # current_version to come back blank. + # Returns a { name => Versionline } hash of the workspace's resolved direct + # dependencies, or nil when the resolved set could not be DETERMINED (pnpm list + # failed, unparseable output, or no matching workspace entry). A workspace that + # genuinely has no resolvable direct deps returns an empty hash, not nil — the + # distinction lets callers drop the libyear union for empty workspaces while not + # wiping data when pnpm itself failed. def add_all_libraries(workspace_path = nil) all_libraries = {} results = run_pnpm_list_depth0 - return all_libraries if results.nil? + return nil if results.nil? begin json = JSON.parse(results) rescue JSON::ParserError - return all_libraries + return nil end packages = json.is_a?(Array) ? json : [json] package = select_workspace_package(packages, workspace_path) - return all_libraries if package.nil? + return nil if package.nil? %w(dependencies devDependencies).each do |group| (package[group] || {}).each do |name, info| diff --git a/openspec/changes/scope-libyear-deps-to-workspace/design.md b/openspec/changes/scope-libyear-deps-to-workspace/design.md index 4d0d9ce..b93cf27 100644 --- a/openspec/changes/scope-libyear-deps-to-workspace/design.md +++ b/openspec/changes/scope-libyear-deps-to-workspace/design.md @@ -41,6 +41,9 @@ Rationale: the resolved set from `pnpm list --json` is already correct and autho **2. Implement by extending `filter_to_workspace_packages`, not rewriting `parse_libyear`.** Move/duplicate the snapshot so it captures the resolved set before `parse_libyear`, and reuse the existing filter to delete out-of-set names. Keeps `parse_libyear` unchanged (it still safely enriches; any entries it adds get filtered out afterward), and unifies libyear-bleed removal with the Dependabot-bleed removal the filter already does. +**2a. Distinguish "could not determine the resolved set" from "resolved but empty".** +`add_all_libraries` returns `nil` when the resolved set cannot be determined (pnpm list failed, unparseable output, or no matching workspace entry) and an (possibly empty) hash on success. The caller snapshots `resolved&.keys&.to_set` (so `nil` propagates) and `filter_to_workspace_packages` skips only when the snapshot is `nil`. An **empty** set still filters — dropping the whole-monorepo libyear union for a workspace that genuinely has no registry-versioned direct deps (e.g. `link:`-only packages like `packages/tsconfig`). Local validation against `jobber-frontend` proved this matters: without the distinction, `apps/harbour`, `packages/tsconfig`, and `packages/graphql-depth-limit-plugin` each re-emitted the full ~215-entry union as blanks; with it, they correctly contribute zero libraries and all 24 workspaces report 0 blank versions. + **3. Option B (per-workspace libyear at the source) rejected for this change.** Making `libyear_.txt` genuinely per-workspace would require working around libyear 0.8.0's `pnpm list --json` + `merge(...)` union behavior in `jobber-frontend`, is cross-repo and more invasive, and still would not retroactively fix data. Option A fixes the ticket symptom and the mis-attribution with a smaller, gem-local blast radius. Option B remains a reasonable future data-pipeline cleanup but is not required. @@ -50,4 +53,5 @@ A dependency that is in the workspace's resolved set but has no semver (e.g. `li ## Risks / Trade-offs - **Outdated transitive or other-workspace deps are no longer surfaced per workspace.** This is intended: they are not dependencies of the analyzed workspace and were previously uploaded as blank-version noise. Aggregate libyear metrics (`meta_data`) are computed in `parse_libyear` before filtering and are unaffected. -- **Reliance on `add_all_libraries` correctness.** If the resolved set is empty (e.g. `pnpm list` failed and returned `nil`), filtering to it would drop everything. Guard: when the resolved set is empty, skip the libyear-scope filter (fall back to current behavior) and log, rather than uploading an empty library list. +- **Reliance on `add_all_libraries` correctness.** Filtering to the resolved set means a mis-resolution would drop libraries. Mitigated by the `nil` (could-not-determine → skip) vs empty (resolved-but-empty → filter) distinction in Decision 2a: a pnpm failure preserves prior behavior instead of wiping, while a genuinely empty workspace correctly drops the foreign union. +- **A workspace with deps that fail to install locally resolves to empty and contributes zero libraries.** Observed for `apps/harbour` in a partial local worktree (`pnpm list --depth=0 --json` returned no installed deps). This is the safe outcome (zero rows beats blank rows); in CI with a full install the workspace resolves its real deps normally. diff --git a/openspec/changes/scope-libyear-deps-to-workspace/specs/pnpm-version-analysis/spec.md b/openspec/changes/scope-libyear-deps-to-workspace/specs/pnpm-version-analysis/spec.md index ffa4d5b..843a824 100644 --- a/openspec/changes/scope-libyear-deps-to-workspace/specs/pnpm-version-analysis/spec.md +++ b/openspec/changes/scope-libyear-deps-to-workspace/specs/pnpm-version-analysis/spec.md @@ -19,7 +19,12 @@ The system SHALL upload library records only for dependencies present in the ana - **WHEN** a dependency is in both the analyzed workspace's resolved dependency set and the libyear report - **THEN** its library record SHALL be retained and SHALL carry both the resolved current version and the libyear-provided latest version -#### Scenario: Resolved set unavailable +#### Scenario: Workspace genuinely has no resolvable direct dependencies -- **WHEN** the analyzed workspace's resolved dependency set cannot be determined (e.g. `pnpm list --json` failed and returned no data) -- **THEN** the system SHALL NOT drop all libraries as out-of-scope, and SHALL instead skip the workspace-scope restriction and log that it was skipped +- **WHEN** the analyzed workspace is resolved successfully but has no registry-versioned direct dependencies (e.g. only `link:`/`workspace:` deps, or none) +- **THEN** the workspace-scope restriction SHALL still apply, so libyear-reported dependencies from other workspaces SHALL be dropped and the workspace SHALL contribute no version-less library records + +#### Scenario: Resolved set cannot be determined + +- **WHEN** the analyzed workspace's resolved dependency set cannot be determined (e.g. `pnpm list --json` failed, returned unparseable output, or contained no matching workspace entry) +- **THEN** the system SHALL distinguish this from an empty-but-resolved set, SHALL NOT drop all libraries as out-of-scope, and SHALL instead skip the workspace-scope restriction and log that it was skipped diff --git a/openspec/changes/scope-libyear-deps-to-workspace/tasks.md b/openspec/changes/scope-libyear-deps-to-workspace/tasks.md index e2aee78..179a7f1 100644 --- a/openspec/changes/scope-libyear-deps-to-workspace/tasks.md +++ b/openspec/changes/scope-libyear-deps-to-workspace/tasks.md @@ -4,7 +4,7 @@ ## 1. Confirm the resolved-set vs libyear-set gap -- [ ] 1.1 From a `jobber-frontend` worktree, capture `add_all_libraries` output for a small workspace (e.g. `packages/tsconfig`) and that workspace's `libyear_packages-tsconfig.txt`; confirm libyear lists ~248 names while the resolved set lists only that workspace's deps. _(Pending live capture; the union behavior was confirmed from libyear 0.8.0 source + the CSV.)_ +- [x] 1.1 From a `jobber-frontend` worktree, capture `add_all_libraries` output for a small workspace (e.g. `packages/tsconfig`) and that workspace's `libyear_packages-tsconfig.txt`; confirm libyear lists ~248 names while the resolved set lists only that workspace's deps. _(Confirmed live: tsconfig resolves 0 registry deps (only a `link:`), while its libyear file held 215 union entries.)_ - [x] 1.2 Confirm the blank rows in the CSV correspond exactly to `libyear_names − resolved_names` for each workspace. _(Confirmed during exploration: 5,975 blank rows; a near-constant ~248-name set blank in every workspace, each name versioned only in the 1–3 workspaces where it is a real dependency.)_ ## 2. Scope the upload to the resolved workspace set @@ -24,8 +24,8 @@ ## 4. Verify against jobber-frontend -- [ ] 4.1 Run analysis for ≥2 workspaces and confirm zero blank `current_version` entries in the upload payload (`server_data`) and that each workspace's library set matches its own resolved deps. -- [ ] 4.2 Spot-check that previously-foreign names (e.g. `@fullcalendar/core` in `packages/tsconfig`) are absent from workspaces where they are not dependencies. +- [x] 4.1 Run analysis for ≥2 workspaces and confirm zero blank `current_version` entries in the upload payload (`server_data`) and that each workspace's library set matches its own resolved deps. _(Ran all 24 jobber-frontend workspaces via a local harness: 0 blank current_version across every workspace; total libraries 1,270 → 625.)_ +- [x] 4.2 Spot-check that previously-foreign names (e.g. `@fullcalendar/core` in `packages/tsconfig`) are absent from workspaces where they are not dependencies. _(Confirmed: empty workspaces (harbour, tsconfig, graphql-depth-limit-plugin) now contribute 0 libraries instead of the ~215-entry union.)_ ## 5. Release & rollout diff --git a/spec/pnpm_spec.rb b/spec/pnpm_spec.rb index 8403e15..34a492f 100644 --- a/spec/pnpm_spec.rb +++ b/spec/pnpm_spec.rb @@ -54,7 +54,9 @@ def do_compare(result:, owner:, current_version:, latest_version:, major:, minor allow(analyzer).to receive(:run_pnpm_list_recursive).and_return(pnpm_list_recursive) allow(analyzer).to receive(:discover_workspaces).and_return([]) allow(analyzer).to receive(:add_dependabot_findings).and_return(nil) - allow(analyzer).to receive(:add_all_libraries).and_return({}) + # nil => resolved set could not be determined, so the workspace-scope filter is skipped + # and the libyear-derived results below are retained (this context exercises that wiring). + allow(analyzer).to receive(:add_all_libraries).and_return(nil) analyzer.get_versions("test") end @@ -494,12 +496,12 @@ def do_compare(result:, owner:, current_version:, latest_version:, major:, minor expect(result["pkg"].current_version).to eq("4.54.0..4.76.0") end - it "returns empty and warns when no workspace entry matches" do + it "returns nil and warns when no workspace entry matches" do expect(analyzer).to receive(:warn).with(/Could not find pnpm list entry/) result = analyzer.send(:add_all_libraries, "/project/apps/does-not-exist") - expect(result).to be_empty + expect(result).to be_nil end it "uses the only entry for a single-package repo (no workspace_path)" do @@ -520,10 +522,28 @@ def do_compare(result:, owner:, current_version:, latest_version:, major:, minor expect(result["lodash"].current_version).to eq("4.17.21") end - it "returns empty on pnpm list failure" do + it "returns nil on pnpm list failure" do allow(analyzer).to receive(:run_pnpm_list_depth0).and_return(nil) - expect(analyzer.send(:add_all_libraries, "/project")).to be_empty + expect(analyzer.send(:add_all_libraries, "/project")).to be_nil + end + + it "returns an empty hash (not nil) when the workspace has no resolvable deps" do + empty_ws = <<~DOC + [ + { + "name": "tsconfig", + "path": "/project", + "dependencies": {}, + "devDependencies": { "sibling": { "version": "link:../sibling" } } + } + ] + DOC + allow(analyzer).to receive(:run_pnpm_list_depth0).and_return(empty_ws) + + result = analyzer.send(:add_all_libraries, "/project") + + expect(result).to eq({}) end end @@ -899,6 +919,28 @@ def do_compare(result:, owner:, current_version:, latest_version:, major:, minor expect(parsed_results).to be_empty end + it "removes everything when the resolved set is empty (no deps in this workspace)" do + parsed_results = { + "react" => LibraryVersionAnalysis::Versionline.new(owner: ":unknown", current_version: ""), + "lodash" => LibraryVersionAnalysis::Versionline.new(owner: ":unknown", current_version: "") + } + + analyzer.send(:filter_to_workspace_packages, parsed_results, Set[], "packages/tsconfig") + + expect(parsed_results).to be_empty + end + + it "skips filtering when the resolved set is nil (pnpm could not determine deps)" do + allow(analyzer).to receive(:warn) + parsed_results = { + "react" => LibraryVersionAnalysis::Versionline.new(owner: ":unknown", current_version: "") + } + + analyzer.send(:filter_to_workspace_packages, parsed_results, nil, "apps/harbour") + + expect(parsed_results).to have_key("react") + end + it "should remove multiple injected packages from different workspaces" do workspace_package_names = Set["react"] @@ -1044,13 +1086,23 @@ def versionline(current) expect(parsed).not_to have_key("@fullcalendar/core") end - it "skips the scope filter (does not drop everything) when the resolved set is empty" do + it "drops the whole libyear union when the workspace resolves to no deps (empty set)" do + # Empty hash = workspace was resolved and genuinely has no registry-versioned direct deps + # (e.g. only link:/workspace: deps). The libyear union must NOT be uploaded as blanks. allow(analyzer).to receive(:add_all_libraries).and_return({}) + + parsed, = analyzer.get_versions_for_workspace("/project/packages/tsconfig", "packages/tsconfig") + + expect(parsed).to be_empty + end + + it "skips the filter (retains libyear data) when the resolved set cannot be determined (nil)" do + allow(analyzer).to receive(:add_all_libraries).and_return(nil) allow(analyzer).to receive(:warn) parsed, = analyzer.get_versions_for_workspace("/project/apps/jobber-online", "apps/jobber-online") - # Prior behavior preserved: libyear-only entries are retained rather than wiped. + # pnpm could not determine deps: keep prior behavior rather than wiping. expect(parsed).to have_key("react") expect(parsed).to have_key("@fullcalendar/core") end From 329811bedbdbca76b43d31af07ae7b513a8d6861 Mon Sep 17 00:00:00 2001 From: John Zittlau Date: Wed, 10 Jun 2026 17:47:25 -0600 Subject: [PATCH 3/5] Declare multi_json in the gemspec so consumers can load the gem (LIBTRACK-136) `check_version_status.rb` always `require "google/apis/sheets_v4"`, and google-api-client -> representable needs multi_json at runtime but doesn't declare it. The earlier fix added multi_json only to this gem's Gemfile, which fixes the gem's own rspec but NOT consumers: jobber-frontend's bin/Gemfile resolves against the gemspec, so `analyze` crashes with "multi_json is not part of the bundle". Move the declaration to the gemspec (single source of truth) so it propagates to any consumer bundle. Gemfile lock is unchanged. rspec: 124 examples, 0 failures; `require "google/apis/sheets_v4"` loads cleanly. Co-Authored-By: Amplify 2.1.1 Co-Authored-By: Claude Opus 4.8 --- Gemfile | 7 +++---- library_version_analysis.gemspec | 5 +++++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/Gemfile b/Gemfile index 8aeadbf..05d9365 100644 --- a/Gemfile +++ b/Gemfile @@ -7,9 +7,8 @@ gem "rspec", "~> 3.0" gem "graphql", "~> 2.4.8" gem "graphql-client", "~> 0.18" -# representable (pulled in via google-api-client) requires "multi_json" at -# runtime but does not declare it as a dependency, so bundler omits it and -# loading google/apis/sheets_v4 fails with "multi_json is not part of the bundle". -gem "multi_json" +# multi_json is declared as a runtime dependency in the gemspec (representable, pulled in via +# google-api-client, requires it at runtime without declaring it). It is intentionally not +# repeated here so the gemspec remains the single source of truth for gem consumers. plugin "bundler-why" diff --git a/library_version_analysis.gemspec b/library_version_analysis.gemspec index c8df5dc..71cfd68 100644 --- a/library_version_analysis.gemspec +++ b/library_version_analysis.gemspec @@ -19,6 +19,11 @@ Gem::Specification.new do |spec| spec.metadata["changelog_uri"] = "https://github.com/GetJobber/library_version_analysis/CHANGELOG.MD" spec.add_dependency 'google-api-client' + # representable (pulled in transitively via google-api-client) requires "multi_json" at runtime + # but does not declare it, so consumers' bundles omit it and `require "google/apis/sheets_v4"` + # fails with "multi_json is not part of the bundle". Declare it here so it propagates to any gem + # consumer (e.g. jobber-frontend's bin/Gemfile), not just this gem's own dev bundle. + spec.add_dependency "multi_json" spec.add_dependency "googleauth" spec.add_dependency "graphql-client", "~> 0.20" spec.add_dependency "libyear-bundler" From 3153c6151b00e15fac282939434a9ff453d5c6b6 Mon Sep 17 00:00:00 2001 From: John Zittlau Date: Wed, 10 Jun 2026 20:48:04 -0600 Subject: [PATCH 4/5] Bump version to 1.5.4 (LIBTRACK-136) Reconciles version.rb (was 1.4.7, behind the consumed v1.5.2 tag) and records the LIBTRACK-136 workspace-scoping + multi_json changes in the changelog. Tag v1.5.4 after merge, then bump jobber-frontend's bin/Gemfile to consume it. Co-Authored-By: Amplify 2.1.1 Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.MD | 8 +++++++- lib/library_version_analysis/version.rb | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.MD b/CHANGELOG.MD index deaf37a..97c7c6d 100644 --- a/CHANGELOG.MD +++ b/CHANGELOG.MD @@ -36,4 +36,10 @@ Updated GQL library requirements. 1.4.7 - Removed graphql dependency. \ No newline at end of file + Removed graphql dependency. + +1.5.4 + Scope libyear-merged dependencies to the analyzed workspace so cross-workspace + deps are no longer uploaded with blank versions (LIBTRACK-136). Distinguish an + undeterminable resolved set (pnpm failure) from an empty one. Declared multi_json + in the gemspec so consumers can load the gem. \ No newline at end of file diff --git a/lib/library_version_analysis/version.rb b/lib/library_version_analysis/version.rb index f885aed..a90036e 100644 --- a/lib/library_version_analysis/version.rb +++ b/lib/library_version_analysis/version.rb @@ -1,3 +1,3 @@ module LibraryVersionAnalysis - VERSION = "1.4.7".freeze + VERSION = "1.5.4".freeze end From 972e7228abd3dbf343a763397bfc953e9782c9db Mon Sep 17 00:00:00 2001 From: John Zittlau Date: Wed, 10 Jun 2026 20:53:54 -0600 Subject: [PATCH 5/5] Archive scope-libyear-deps-to-workspace; promote spec (LIBTRACK-136) Folds the workspace-scoping requirement into the pnpm-version-analysis spec and moves the change under openspec/changes/archive/. Release/rollout tasks (5.x) remain as post-merge follow-ups, consistent with how the prior LIBTRACK-136 fix was archived. Co-Authored-By: Amplify 2.1.1 Co-Authored-By: Claude Opus 4.8 --- .../.openspec.yaml | 0 .../design.md | 0 .../proposal.md | 0 .../specs/pnpm-version-analysis/spec.md | 0 .../tasks.md | 0 openspec/specs/pnpm-version-analysis/spec.md | 29 +++++++++++++++++++ 6 files changed, 29 insertions(+) rename openspec/changes/{scope-libyear-deps-to-workspace => archive/2026-06-11-scope-libyear-deps-to-workspace}/.openspec.yaml (100%) rename openspec/changes/{scope-libyear-deps-to-workspace => archive/2026-06-11-scope-libyear-deps-to-workspace}/design.md (100%) rename openspec/changes/{scope-libyear-deps-to-workspace => archive/2026-06-11-scope-libyear-deps-to-workspace}/proposal.md (100%) rename openspec/changes/{scope-libyear-deps-to-workspace => archive/2026-06-11-scope-libyear-deps-to-workspace}/specs/pnpm-version-analysis/spec.md (100%) rename openspec/changes/{scope-libyear-deps-to-workspace => archive/2026-06-11-scope-libyear-deps-to-workspace}/tasks.md (100%) diff --git a/openspec/changes/scope-libyear-deps-to-workspace/.openspec.yaml b/openspec/changes/archive/2026-06-11-scope-libyear-deps-to-workspace/.openspec.yaml similarity index 100% rename from openspec/changes/scope-libyear-deps-to-workspace/.openspec.yaml rename to openspec/changes/archive/2026-06-11-scope-libyear-deps-to-workspace/.openspec.yaml diff --git a/openspec/changes/scope-libyear-deps-to-workspace/design.md b/openspec/changes/archive/2026-06-11-scope-libyear-deps-to-workspace/design.md similarity index 100% rename from openspec/changes/scope-libyear-deps-to-workspace/design.md rename to openspec/changes/archive/2026-06-11-scope-libyear-deps-to-workspace/design.md diff --git a/openspec/changes/scope-libyear-deps-to-workspace/proposal.md b/openspec/changes/archive/2026-06-11-scope-libyear-deps-to-workspace/proposal.md similarity index 100% rename from openspec/changes/scope-libyear-deps-to-workspace/proposal.md rename to openspec/changes/archive/2026-06-11-scope-libyear-deps-to-workspace/proposal.md diff --git a/openspec/changes/scope-libyear-deps-to-workspace/specs/pnpm-version-analysis/spec.md b/openspec/changes/archive/2026-06-11-scope-libyear-deps-to-workspace/specs/pnpm-version-analysis/spec.md similarity index 100% rename from openspec/changes/scope-libyear-deps-to-workspace/specs/pnpm-version-analysis/spec.md rename to openspec/changes/archive/2026-06-11-scope-libyear-deps-to-workspace/specs/pnpm-version-analysis/spec.md diff --git a/openspec/changes/scope-libyear-deps-to-workspace/tasks.md b/openspec/changes/archive/2026-06-11-scope-libyear-deps-to-workspace/tasks.md similarity index 100% rename from openspec/changes/scope-libyear-deps-to-workspace/tasks.md rename to openspec/changes/archive/2026-06-11-scope-libyear-deps-to-workspace/tasks.md diff --git a/openspec/specs/pnpm-version-analysis/spec.md b/openspec/specs/pnpm-version-analysis/spec.md index d6a1c77..5ba8f57 100644 --- a/openspec/specs/pnpm-version-analysis/spec.md +++ b/openspec/specs/pnpm-version-analysis/spec.md @@ -55,3 +55,32 @@ The system SHALL continue to merge resolved current versions with libyear data a - **WHEN** a tracked library has no resolvable installed version (e.g. a workspace `link:` dependency) - **THEN** the system SHALL record an empty current version for that library without aborting analysis of the remaining libraries +### Requirement: Tracked libraries are limited to the analyzed workspace's dependencies + +The system SHALL upload library records only for dependencies present in the analyzed workspace's resolved dependency set (the `dependencies` and `devDependencies` resolved from that workspace's `pnpm list --json`). Dependencies that appear only in the libyear report — including dependencies that belong to other workspaces because the libyear report spans the whole monorepo — SHALL NOT be added as standalone library records. + +#### Scenario: libyear report includes dependencies from other workspaces + +- **WHEN** the libyear report used for workspace `packages/tsconfig` includes `@fullcalendar/core`, which is a dependency of `apps/jobber-online` and not of `packages/tsconfig` +- **THEN** `@fullcalendar/core` SHALL NOT appear as a library record in the `packages/tsconfig` results + +#### Scenario: No blank-version records from libyear-only dependencies + +- **WHEN** the libyear report contains a dependency that has no entry in the analyzed workspace's resolved dependency set +- **THEN** the system SHALL NOT create a library record with an empty current version for that dependency + +#### Scenario: In-workspace dependency present in libyear is retained and enriched + +- **WHEN** a dependency is in both the analyzed workspace's resolved dependency set and the libyear report +- **THEN** its library record SHALL be retained and SHALL carry both the resolved current version and the libyear-provided latest version + +#### Scenario: Workspace genuinely has no resolvable direct dependencies + +- **WHEN** the analyzed workspace is resolved successfully but has no registry-versioned direct dependencies (e.g. only `link:`/`workspace:` deps, or none) +- **THEN** the workspace-scope restriction SHALL still apply, so libyear-reported dependencies from other workspaces SHALL be dropped and the workspace SHALL contribute no version-less library records + +#### Scenario: Resolved set cannot be determined + +- **WHEN** the analyzed workspace's resolved dependency set cannot be determined (e.g. `pnpm list --json` failed, returned unparseable output, or contained no matching workspace entry) +- **THEN** the system SHALL distinguish this from an empty-but-resolved set, SHALL NOT drop all libraries as out-of-scope, and SHALL instead skip the workspace-scope restriction and log that it was skipped +