Skip to content

Commit 23d74c0

Browse files
Surface present-but-unparseable workspace managers in architecture discovery (#558) (#561)
* Surface present-but-unparseable workspace managers in architecture discovery (UWP4XK) Closes #558. A workspace manager present at the repo root but unparseable (malformed go.work, unreadable Cargo [workspace] members, flow-style pnpm-workspace.yaml) used to return undefined from its detector — conflating "absent" with "present-but-unparseable" — and contribute zero packages with no marker, so a whole language could vanish from the map at the discovery layer, before the ZRW21K per-package "not introspected" marker could fire. Each detector now returns a discriminated `absent | parsed | unreadable`. The unreadable set is surfaced advisory-only: a `## Coverage gaps` section in the generated root index naming each unreadable config, plus a non-blocking `safeword architecture` / `--check` warning. It is folded into the root fingerprint only when non-empty, so a clean repo (this one) never re-renders. Reuses the existing union+dedupe discovery pipeline and toml table readers; no new detector module, no blocking gate. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NRGYnMkxo3yim7qdFs7s19 * Refine unreadable-workspace detection to avoid valid-config false alarms (UWP4XK) From the independent /quality-review of #558. The detector classified a workspace as "unreadable" whenever the manager's table/file was present but yielded no parseable globs — false-alarming on valid configs where the member-list declaration is simply absent. Narrowed to: unreadable only when the member-list declaration is PRESENT but unparseable. - Cargo [workspace] with no `members` key -> absent (valid root-package auto-discovery / default-members; verified vs the Cargo reference). New hasTomlTableKey helper. - pnpm-workspace.yaml with no `packages:` key -> absent (catalog/settings-only is valid; verified vs the pnpm reference). - go.work with no `use` directive -> absent (a fresh `go work init` file). - uv unchanged: `members` is required in [tool.uv.workspace], so a table with no usable members is genuinely malformed. Added U9/U10/U11 asserting each valid case is absent, not a false alarm. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NRGYnMkxo3yim7qdFs7s19 * refactor: extract parsed()/unreadable() constructors for WorkspaceDetection The discriminated-union variants were built inline as object literals at five detector sites each — so changing the shape meant editing five places. Extract two small constructors and route every detector through them; each detector still decides WHICH variant to return, so no per-manager semantics are lost. Behavior-identical (52 monorepo + 27 document tests green). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NRGYnMkxo3yim7qdFs7s19 * chore(UWP4XK): advance ticket to phase verify (verification complete) Verification complete — 55 monorepo unit tests + 14 architecture scenarios, full CI green, real-CLI polyglot live-fire (incl. the false-alarm fixes), and an independent quality-review (false-alarm fix applied). Matches the merged MGWZ4P example's final state (phase: verify, status: todo); completion is the PR #561 merge. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NRGYnMkxo3yim7qdFs7s19 --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 645e2a6 commit 23d74c0

13 files changed

Lines changed: 969 additions & 53 deletions

File tree

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Behavioral dimensions — UWP4XK (present-but-unparseable workspace managers)
2+
3+
Behavior: `discoverWorkspaces(dir) → { patterns, unreadable[] }`, and how the
4+
`unreadable[]` set is surfaced (root-index advisory, `architecture`/`--check`
5+
warning) and folded into the root fingerprint. The input space is "for each
6+
manager at the root, is it absent, parseable, or present-but-unparseable, and do
7+
any OTHER managers still yield packages."
8+
9+
| # | Dimension | Partitions | Covered by |
10+
| - | --------- | ---------- | ---------- |
11+
| D1 | **Per-manager detection outcome** | (a) absent; (b) parsed; (c) present-but-unparseable | U1–U5 (one per manager × each outcome) |
12+
| D2 | **Which manager is unparseable** | (a) go.work; (b) Cargo `[workspace]`; (c) uv `[tool.uv.workspace]`; (d) pnpm flow-style; (e) package.json malformed shape | U1/AC1, U2/AC2(Cargo), U3, U4, U5 |
13+
| D3 | **Co-present readable manager** | (a) none (only the unreadable one) → no root index, warning-only surface; (b) ≥1 other readable → root index lists readable packages + carries the advisory | AC1–AC3 + U6 (b); AC4 (a: warning-only) |
14+
| D4 | **Absent vs present-but-unparseable boundary** | (a) manifest absent → absent; (b) workspace table/field absent → absent; (c) **member-list key absent though a table exists** (Cargo `[workspace]` no `members` / pnpm no `packages:` / go.work no `use`) → absent, NOT a false alarm; (d) member-list key present but unparseable → unreadable; (e) explicitly-empty (e.g. `workspaces: []`) → absent | U7 (Cargo no-table), U8 (empty array), U9 (Cargo no-members), U10 (pnpm catalog-only), U11 (go.work no-use); U1–U5 (d) |
15+
| D5 | **Fingerprint freshness** | (a) unreadable set empty → fingerprint unchanged vs today (no churn); (b) unreadable appears/disappears → fingerprint moves | FP1/FP2 (fingerprint tests) |
16+
| D6 | **Surface non-blocking** | (a) `architecture` exit unchanged; (b) `--check` exit unchanged (advisory ≠ stale) | AC4 (command warns, exit 0) |
17+
18+
## Load-bearing partitions (where this change actually bites)
19+
20+
- **D1(c) for every manager (D2 a–e)** — the bug: a present-but-unparseable
21+
manifest must become an `unreadable` signal, not `undefined`. New behavior.
22+
- **D3(b)** — the readable side is never blinded: a malformed go.work next to a
23+
working manager still lists the working packages AND names go.work. This is the
24+
coverage-honesty property (the union from #554 must survive one bad manager).
25+
- **D4(b) vs D4(c)** — the boundary that must NOT over-fire: a `Cargo.toml` with no
26+
`[workspace]` (single crate) stays absent; only a present table with unparseable
27+
members is unreadable. Guards against advisory false alarms.
28+
- **D5(a)** — no churn: a repo with zero unreadable configs keeps its fingerprint.
29+
30+
## Out-of-partition (not exercised; noted)
31+
32+
Nested/recursive workspaces (a member that is itself a workspace root) — out of
33+
scope; current behavior (treat as one leaf, or not discovered) is unchanged.
34+
Repairing the unparseable manifest — never attempted.
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# Spec: Surface a present-but-unparseable workspace manager (UWP4XK)
2+
3+
## Jobs To Be Done
4+
5+
### UWP4XK.J1 — A polyglot map that admits what it could not read
6+
7+
**Persona:** Technical Builder (TB)
8+
9+
When safeword discovers my monorepo and one workspace manager's root manifest is
10+
present but unparseable (a malformed `go.work`, an unreadable Cargo `[workspace]
11+
members`, a flow-style `pnpm-workspace.yaml`), I want the generated map to **say
12+
so out loud** instead of silently listing zero packages for it, so I can trust
13+
"the map is complete" or see exactly which config to fix. (The NTB can't audit
14+
the diff to notice the Go services are missing — silence reads as "complete.")
15+
16+
#### UWP4XK.J1.AC1 — Present-but-unparseable is surfaced, not dropped
17+
18+
A root manifest that declares a workspace whose member list can't be parsed is
19+
named as unreadable (manager + config file) wherever the architecture surface is
20+
shown — it never contributes zero packages with no marker.
21+
22+
#### UWP4XK.J1.AC2 — Absent stays silent
23+
24+
A manager that is genuinely absent (no `go.work`; a `Cargo.toml`/`pyproject.toml`
25+
with no workspace table; a single-crate/single-package repo) produces no
26+
unreadable signal — only *present-but-unparseable* is surfaced, so the advisory
27+
is never a false alarm.
28+
29+
#### UWP4XK.J1.AC3 — The readable side is never blinded
30+
31+
When other managers are readable, their packages are still discovered and listed;
32+
the unreadable manager is surfaced *alongside* them (an advisory in the rendered
33+
root index), not in place of them. One unreadable config never drops a readable
34+
manager's packages.
35+
36+
#### UWP4XK.J1.AC4 — Advisory, never blocking
37+
38+
The surface is advisory: `safeword architecture` and `architecture --check` print
39+
a warning but their exit codes are unchanged; the root index renders the advisory
40+
but no command fails. The human doc / the build is never blocked on it.
41+
42+
#### UWP4XK.J1.AC5 — The advisory stays fresh
43+
44+
The root-index advisory re-renders when an unreadable config appears or is fixed
45+
(the unreadable set is part of the root fingerprint). A repo with no unreadable
46+
config keeps its existing fingerprint — no churn.
47+
48+
## Problem
49+
50+
`discoverLeafDirectories` unions managers (#554), but each detector returns
51+
`string[] | undefined` where `undefined` means BOTH "absent" and
52+
"present-but-unparseable." The unparseable case is filtered out at discovery,
53+
before the per-package "not introspected" marker (ZRW21K) can fire — a manager
54+
that discovered nothing has nothing to mark. So the set vanishes silently.
55+
56+
## Design
57+
58+
Replace each detector's `string[] | undefined` with a discriminated
59+
`WorkspaceDetection`:
60+
61+
- `{ status: 'absent' }` — no workspace declared (no signal).
62+
- `{ status: 'parsed'; patterns: string[] }` — globs/dirs to expand (today's path).
63+
- `{ status: 'unreadable'; manager; config }` — a workspace is declared but its
64+
member list can't be parsed.
65+
66+
What counts as *declared but unreadable* per manager — the member-list declaration
67+
is PRESENT but can't be parsed (a *missing* declaration is absent, never a false
68+
alarm):
69+
70+
- **go.work**: a `use` directive present but zero member dirs parse (malformed/junk
71+
`use`). No `use` directive at all ⇒ absent (a `go work init` file with no modules).
72+
- **Cargo `[workspace]`**: `[workspace]` + a `members` key present but unparseable.
73+
No `[workspace]` ⇒ absent (single crate); `[workspace]` with no `members` key ⇒
74+
absent (a valid root-package workspace that auto-discovers members from path deps /
75+
`default-members` — out of scope like nested workspaces, verified against the Cargo
76+
reference).
77+
- **uv**: `[tool.uv.workspace]` table present but `members` unparseable (no table ⇒
78+
absent). uv *requires* `members`, so a table with no usable members is genuinely
79+
malformed → unreadable.
80+
- **pnpm**: a `packages:` key present but no glob parses (flow-style, empty block).
81+
No `packages:` key ⇒ absent (a catalog-only/settings-only file is valid and declares
82+
no members — verified against the pnpm reference).
83+
- **package.json**: a `workspaces` field present but not a usable array /
84+
`{packages: [...]}` (malformed shape) ⇒ unreadable; an explicitly empty array
85+
⇒ absent (deliberate). JS precedence (package.json wins over pnpm) is preserved:
86+
package.json present — parsed OR unreadable — wins; only an absent package.json
87+
falls through to pnpm.
88+
89+
`discoverWorkspaces(dir)` returns `{ patterns, unreadable[] }`. `discoverLeafDirectories`
90+
becomes a thin wrapper over `.patterns` (unchanged behavior + signature).
91+
`extractMonorepoModel` gains `unreadableWorkspaces[]`; `monorepoFingerprint`
92+
includes it only when non-empty. The root index renders a `## Coverage gaps`
93+
advisory; `architecture`/`--check` print a non-blocking warning.
94+
95+
## Out of scope
96+
97+
Nested/recursive workspaces; new manifest formats; per-leaf extraction; any
98+
blocking gate; repairing the unparseable manifest.
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# Test definitions (R/G/R ledger) — UWP4XK
2+
3+
Behavior: a workspace manager PRESENT at the root but unparseable is surfaced
4+
(root-index `## Coverage gaps` advisory + `architecture`/`--check` warning),
5+
never silently dropped. Black-box scenarios in
6+
`features/architecture-unreadable-workspace.feature`; detector precision in
7+
`tests/utils/architecture-monorepo.test.ts`. Dimensions in dimensions.md.
8+
9+
## Black-box scenarios (acceptance lane — `@architecture-unreadable-workspace`)
10+
11+
- [x] **AC1 — malformed go.work + working JS** (`…UWP4XK.AC1`) [J1.AC1/AC3, D2(a), D3(b)]
12+
- RED (pre-fix): go.work returns `undefined`, contributes nothing, no marker; root
13+
index lists `web` only, NO advisory. Failure: "root index has no Coverage gaps".
14+
- GREEN: root index lists `web` AND a `## Coverage gaps` advisory names `go.work`.
15+
- [x] **AC2 — unreadable Cargo [workspace] members + uv Python** (`…UWP4XK.AC2`) [J1.AC1/AC3, D2(b), D3(b)]
16+
- RED: Cargo `undefined` (absent==unparseable), dropped silently; no advisory.
17+
- GREEN: lists `pytool` AND advisory names `Cargo.toml`.
18+
- [x] **AC3 — flow-style pnpm + Go module** (`…UWP4XK.AC3`) [J1.AC1/AC3, D2(d), D3(b)]
19+
- RED: pnpm flow-style → `undefined`, dropped; no advisory (today's silent
20+
"degrades gracefully" path in monorepo-coverage-honesty).
21+
- GREEN: lists `gosvc` AND advisory names `pnpm-workspace.yaml`.
22+
- [x] **AC4 — only-unparseable case is warned, not blocked** (`…UWP4XK.AC4`) [J1.AC4, D3(a), D6]
23+
- RED: a lone malformed go.work → single-repo doc, zero signal, command silent.
24+
- GREEN: command exits 0 AND its output warns that `go.work` is unreadable
25+
(the warning channel covers the no-root-index path).
26+
- [x] **AC5 — absent is not a false alarm** (`…UWP4XK.AC5`) [J1.AC2, D4(b)]
27+
- Negative case: a single-crate `Cargo.toml` (no `[workspace]`) must NOT raise an
28+
advisory. GREEN: lists `gosvc`, no `## Coverage gaps` section.
29+
30+
Each scenario is Arrange-Observe-Decide-Independent: the Given builds the repo, the
31+
When runs the real CLI, the Then reads only observable output (the rendered doc or the
32+
command's stdout/stderr) — no package internals.
33+
34+
## Unit tests (detector precision — tests/utils/architecture-monorepo.test.ts)
35+
36+
- [x] **U1 — malformed go.work is unreadable, not absent** [D1(c), D2(a)]
37+
- RED: detector returned `undefined`; `discoverWorkspaces` had no `unreadable` field.
38+
GREEN: `{ patterns: [], unreadable: [{go.work}] }`.
39+
- [x] **U2 — Cargo [workspace] present, members unparseable → unreadable** [D1(c), D2(b)]
40+
- [x] **U3 — uv [tool.uv.workspace] present, members unparseable → unreadable** [D1(c), D2(c)]
41+
- [x] **U4 — flow-style pnpm → unreadable** [D1(c), D2(d)]
42+
- [x] **U5 — package.json `workspaces` of wrong shape → unreadable** [D1(c), D2(e)]
43+
- [x] **U6 — a malformed manager never blinds the readable ones** [J1.AC3, D3(b)]: JS
44+
`web` + malformed go.work → patterns keep `packages/*`, leaves keep `web`, unreadable
45+
records `go.work`. The coverage-honesty property (#554 union survives one bad manager).
46+
- [x] **U7 — single-crate Cargo.toml (no [workspace]) raises no signal** [J1.AC2, D4(b)]
47+
- [x] **U8 — explicitly-empty `workspaces: []` is absent, not unreadable** [D4(e)]
48+
- [x] **U9 — Cargo `[workspace]` with no `members` key (auto-discovery) is absent** [J1.AC2, D4(c)]:
49+
a root-package workspace using `default-members`/path-dep auto-discovery is valid, not a
50+
coverage gap — must not false-alarm. (quality-review finding; verified vs the Cargo reference.)
51+
- [x] **U10 — catalog-only `pnpm-workspace.yaml` (no `packages:` key) is absent** [J1.AC2, D4(c)]:
52+
`packages:` is optional in pnpm; a catalog/settings-only file declares no members.
53+
(quality-review finding; verified vs the pnpm reference.)
54+
- [x] **U11 — `go.work` with no `use` directive (fresh init) is absent** [J1.AC2, D4(c)]:
55+
a member-less go.work declares nothing — only a present-but-junk `use` is unreadable.
56+
- [x] **model — `extractMonorepoModel` exposes `unreadableWorkspaces`** [feeds render + fingerprint]
57+
- [x] **FP1 — fingerprint moves when an unreadable manager appears** [D5(b)]
58+
- [x] **FP2 — fingerprint does NOT move for a clean repo (no churn)** [D5(a)]: the
59+
unreadable key is contributed only when non-empty, so a readable-only repo keeps its hash.
60+
61+
## Reconcile note
62+
63+
Coverage honesty (no silent drop) is the union of U1–U6 + AC1–AC4: every present
64+
cross-ecosystem manager that can't be parsed reaches a surface (doc advisory and/or
65+
command warning). U7/U8 + AC5 pin the absent/empty boundary so the advisory is never a
66+
false alarm. U9/U9b keep the doc advisory fresh without churning clean repos.
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
---
2+
id: UWP4XK
3+
slug: unparseable-workspace-coverage
4+
type: feature
5+
phase: verify
6+
status: todo
7+
created: 2026-06-29T19:30:00.000Z
8+
last_modified: 2026-06-30T04:11:00.000Z
9+
scope:
10+
- Distinguish "workspace manager absent" from "workspace manager present-but-unparseable" at the detector boundary in `architecture-monorepo.ts` (`detectGoWork`, `detectCargoWorkspace`, `detectUvWorkspace`, the JS package.json/pnpm detectors). Today each returns `string[] | undefined` and `undefined` collapses both cases, so a present-but-unreadable root manifest contributes zero packages with no marker.
11+
- SURFACE a present-but-unparseable workspace config (advisory only, never blocking) in two places: a `## Coverage gaps` advisory line in the rendered monorepo root index naming the unreadable config, and a `safeword architecture` / `architecture --check` warning naming the unreadable config.
12+
- Fold the unreadable-workspace set into `monorepoFingerprint` (only when non-empty, to avoid churning repos that have none) so the root-index advisory stays fresh as an unreadable config appears or is fixed.
13+
out_of_scope:
14+
- Nested/recursive workspaces (a member that is itself a workspace root).
15+
- New language packs / manifest formats; per-leaf polyglot extraction (unchanged).
16+
- Any blocking gate — the surface is advisory like the rest of the architecture honesty layer.
17+
- Repairing or guessing the contents of an unparseable manifest; we only report it.
18+
done_when:
19+
- A repo with a malformed `go.work` (a `use` directive that yields no member dir) alongside a working manager surfaces the `go.work` as unreadable — its presence is named, not silently dropped.
20+
- A repo with a `Cargo.toml` `[workspace]` whose `members` array can't be parsed surfaces `Cargo.toml` as unreadable; a `Cargo.toml` with NO `[workspace]` table (a single crate) stays silent (absent, not unreadable).
21+
- A repo with a flow-style `pnpm-workspace.yaml` surfaces `pnpm-workspace.yaml` as unreadable.
22+
- When at least one OTHER manager still yields packages, the rendered root index carries a `## Coverage gaps` advisory naming each unreadable config AND lists the packages the readable managers discovered (the readable side is never blinded by the unreadable one).
23+
- `safeword architecture` and `architecture --check` print a non-blocking warning naming each unreadable config; neither command's exit code changes because of it.
24+
- `monorepoFingerprint` moves when an unreadable config appears or is fixed, but a repo with no unreadable config (this repo) keeps its existing fingerprint — no dogfood churn beyond the one-time regeneration this ticket lands.
25+
- No regression: absent managers, single-manager monorepos, and single-repo projects are unchanged; `architecture --check` still passes on this repo.
26+
---
27+
28+
# Surface a present-but-unparseable workspace manager (coverage honesty)
29+
30+
**Goal:** A workspace manager that is *present* at the repo root but whose member
31+
list safeword cannot parse must be **surfaced**, not silently dropped. Today it
32+
contributes zero packages and no marker — the same class of silent omission #554
33+
fixed (a whole language vanishing from the map), one layer up.
34+
35+
**Origin:** GitHub #558, surfaced by the independent quality-review of #554
36+
(MGWZ4P). Pre-existing: each detector degraded this way under the old first-match
37+
chain too; explicitly out of scope for #554, filed so it isn't lost.
38+
39+
## Problem
40+
41+
`discoverLeafDirectories` unions all workspace managers (#554), but each detector
42+
returns `string[] | undefined`, and `undefined` means **both "absent" and
43+
"present-but-unparseable."** A malformed `go.work`, an unreadable Cargo
44+
`[workspace] members`, or a flow-style `pnpm-workspace.yaml` returns `undefined`,
45+
is filtered out, and contributes zero packages with no marker. The package set
46+
vanishes at the **discovery** layer — *before* the ZRW21K "not introspected"
47+
marker can fire (that marker is per-discovered-package; a manager that discovered
48+
nothing has nothing to mark). The root index looks complete while silently
49+
omitting, say, every Go service.
50+
51+
safeword's coverage-honesty principle is "incomplete is fine, silently wrong is
52+
not." A present manager that can't be parsed should be surfaced.
53+
54+
## Decision
55+
56+
Distinguish the two cases at the detector boundary with a small discriminated
57+
return (`absent` | `parsed` | `unreadable`), carrying the manager label and
58+
config filename on the `unreadable` case so discovery can surface it. Reuse the
59+
existing glob-expand + `hasRecognizedManifest` + `Set`-dedupe pipeline unchanged
60+
— the readable managers still union exactly as before; the only addition is a
61+
parallel `unreadable[]` channel. Surface it advisory-only (root-index line +
62+
`--check`/command warning) and fold it into the root fingerprint (non-empty only)
63+
so the advisory stays fresh. Never block.
64+
65+
## Next
66+
67+
`/bdd`: spec → dimensions → scenarios (malformed go.work, unreadable Cargo
68+
members, flow-style pnpm — each surfaced, not silently empty) → independent
69+
scenario review → TDD → verify → audit.

0 commit comments

Comments
 (0)