Skip to content

[US-384] chore: mirror-equality guard widened to every dataset skill artifact - #406

Merged
rucka merged 5 commits into
mainfrom
chore/US-384-mirror-guard-all-skill-artifacts
Jul 31, 2026
Merged

[US-384] chore: mirror-equality guard widened to every dataset skill artifact#406
rucka merged 5 commits into
mainfrom
chore/US-384-mirror-guard-all-skill-artifacts

Conversation

@rucka

@rucka rucka commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

PR Title: [US-384] chore: mirror-equality guard widened to every dataset skill artifact
Story/Epic: Closes #384 (builds on #352, PR #377)
Type: Chore (tech-debt)
Labels: tech-debt, risk:yellow
Priority: P2

Summary

What Changed

Widens the #352 mirror-equality guard (packages/knowledge-hub/src/tools/skill-md-mirror.ts + colocated .test.ts) from asserting only each skill's SKILL.md to asserting EVERY markdown artifact a dataset skill dir contributes through the real pair update copy pipeline — sub-docs today (process/bootstrap/assess-orchestration.md, process/review/merge-and-cascade.md, process/review/degradation-levels.md, process/implement/post-review-merge.md), any future references/* automatically.

Why This Change

The #352 guard deliberately scoped to SKILL.md; a hand-edit to a root sub-doc could drift silently from its dataset source until someone regenerated and lost it. This closes that gap the same way #352 closed it for SKILL.md: real pipeline, no parallel transform re-implementation, data-driven so it never needs a test edit as artifacts are added.

Story Context

User Story: As a maintainer of the Pair knowledge base, I want the mirror-equality guard to assert every artifact a skill directory contributes, not only its SKILL.md, so a hand-edit to a root sub-doc is caught in CI instead of drifting silently.
Acceptance Criteria: AC1-AC5, see mapping below.

Changes Made

Implementation Details

  • datasetSkillArtifacts(tree) — every .md path the dataset contributes, recursive, markdown-only, sorted, dataset-derived — never a hardcoded list, never a count (AC1/AC3).
  • installedArtifactPath(datasetArtifact) — root location via the REAL transformPath, correctly flattening a nested sub-directory into its own top-level prefixed dir (process/review/references/deep.mdpair-process-review-references/deep.md) exactly as the copy pipeline does.
  • buildInstalledArtifacts(tree) — one real copyDirectoryWithTransforms run over an in-memory dataset clone → { byDatasetPath, producedPaths }. producedPaths is the pipeline's ACTUAL output set, cross-checked set-for-set against installedArtifactPath's derivation, so a wrong path-mapping assumption can no longer silently drop an artifact from the assertion instead of failing loudly.
  • assertRootArtifactMatches — same contract as tech-debt: extend mirror-equality guard to per-skill SKILL.md pairs (all 36 skills) #352's assertRootSkillMdMatches (names the artifact by dataset path, compact diffSkillMd line diff, "missing → run pair update" hint), now the single assertion helper for both SKILL.md and every sub-doc.
  • Drift-injection extended to non-SKILL.md artifacts on a synthetic demo/sub-doc.md + demo/references/deep.md fixture (hermetic, not the real 4): content drift, sibling self-pointer normalisation (SKILL.md./SKILL.md), link-depth bump, /command skill-reference rewrite, missing root copy — each proven RED then green.
  • Directional guard behavior (dataset → root, root-only files never asserted) re-verified against the wider artifact set.

Files Changed

  • Modified: packages/knowledge-hub/src/tools/skill-md-mirror.ts (extended production helper)
  • Modified: packages/knowledge-hub/src/tools/skill-md-mirror.test.ts (extended guard + drift-injection suite)

No other files touched — confirmed disjoint from every other currently open PR's file footprint (#386-391, #404, #405).

Testing

Test Coverage

  • Unit Tests: packages/knowledge-hub full suite: 542 tests pass, 78 in skill-md-mirror.test.ts. Baseline for the delta is current origin/main, measured, not tech-debt: extend mirror-equality guard to per-skill SKILL.md pairs (all 36 skills) #352's merge-time count: 54 → 78 (+24) — main is 14 static cases + it.each(skillDirs) over 40 dataset skill dirs; this branch is 34 static cases + it.each(artifacts) over 44 dataset markdown artifacts (40 SKILL.md + 4 sub-docs).
  • Manual/Injection Testing: every new assertion class proven RED (drift injected) then GREEN (reconciled) via the drift-injection suite — see AC mapping.

AC → Test Mapping

AC Covered by
AC1 (every artifact asserted byte-for-byte) it.each(artifacts) main guard — 44 cases (main asserts 40, SKILL.md-only)
AC2 (named failure + compact diff on drift) drift-injection: sub-doc content/self-pointer/link-depth//command drift tests
AC3 (data-driven, no count; new artifact covered with no test edit) datasetSkillArtifacts derivation + nested-subdir recursion test
AC4 (missing root copy fails loudly, points at pair update) drift-injection: FAILS loudly when the sub-doc root copy is missing / nested-reference variant; plus root-copy read distinguishes a missing copy from an unreadable one (EXISTS-but-unreadable is rethrown with its path + cause, never mislabelled "missing → run pair update")
AC5 (directional, root-only file never asserted) directional guard ignores root-only artifacts...agent-browser (whole root-only skill) and a stray scratch.md really written into the pipeline's destination tree (InstalledMirror.root.write, then the produced set re-derived) so the case is a fixture, not an absent-by-construction assertion

AC numbering in code comments is #384's throughout (AC2 = drift, AC3 = data-driven, AC4 = missing/unreadable root copy, AC5 = directional); #352's numbering is no longer referenced anywhere in the file.

Test Results

Test Suite: ✅ Passing — pnpm --filter @pair/knowledge-hub test (542 tests, incl. 78 in skill-md-mirror.test.ts)
Lint: ✅ Clean — pnpm --filter @pair/knowledge-hub lint
Type-check: ✅ Clean — pnpm --filter @pair/knowledge-hub ts:check
Full quality gate: ✅ Green — pnpm quality-gate (run once, end of session; all turbo tasks + hygiene/docs-staleness/skills-conformance/dup:check pass)

T-6 — Reconciling pre-existing drift (mandatory per the story, precedent #352 → root carried a fix the dataset lacked)

Outcome: no drift found, nothing reconciled. Ran the extended guard against the current main state for all 4 real sub-docs — all pass. Traced git log on the one artifact a prior investigation flagged (assess-orchestration.md), per copy — the two sides have DIFFERENT last-touch commits:

Copy Last touched by
root .claude/skills/pair-process-bootstrap/assess-orchestration.md 7bc2f50c — "make pair update self-consistent for the dogfooded root install" (PR #346)
dataset packages/knowledge-hub/dataset/.skills/process/bootstrap/assess-orchestration.md 66cbfc44 — progressive disclosure for implement/review/bootstrap (PR #331)

So the ROOT side was reconciled by #346 (which changed only the root copy) while the DATASET side is unchanged since #331 — i.e. #346 brought the root copy back in line with the then-current dataset source, and the pair has been consistent ever since. Nothing is left to reconcile in this PR. The verdict does not rest on that history: it is independently verified by the extended guard passing against the real dataset for all 4 sub-docs. Flagged explicitly because the story's DoD requires T-6 to be reported even when the finding is "nothing to reconcile," so a reviewer doesn't go looking for a reconciliation commit that isn't there.

Known Behavior & Accepted Residuals (decided, recorded in the module docblock)

  • Nested references/ flatten is a DEFECT, not a sanctioned layout — tracked as tech-debt: pair update mis-installs a skill's nested references/ subdir (flattened out of the skill, both links broken) #407. The pipeline installs process/review/references/deep.md as pair-process-review-references/deep.md (outside its skill dir, both links broken). This guard mirrors that behavior faithfully so it cannot lie about the mirror; the docblock on installedArtifactPath, the test-suite docblock and the test name all point at tech-debt: pair update mis-installs a skill's nested references/ subdir (flattened out of the skill, both links broken) #407 so the next author adding a references/ sub-doc does not read the mapping as endorsed. tech-debt: pair update mis-installs a skill's nested references/ subdir (flattened out of the skill, both links broken) #407's fix flips these expectations.
  • Orphans: accepted residual — and its premise is now pinned. The guard stays directional and pair update copies with behavior overwrite (no mirror-delete; the resolved options are asserted against the registry's declared behavior by the SKILL_COPY_OPTS pin test, via skillCopySyncOptions(), so a flip to mirror fails there instead of quietly invalidating this residual), so a sub-doc deleted from the dataset but left in .claude/skills/<skill>/ is neither asserted nor cleaned. Detecting it needs a non-directional check that must exempt root-only skills (agent-browser); that inversion is deliberately out of this guard's contract ("every dataset artifact is faithfully mirrored") — regenerating deletions belongs to pair update. Recorded as an ACCEPTED RESIDUAL block in the module docblock.
  • Non-markdown: two distinct reasons, kept apart. (1) junk (.DS_Store, editor backups) must NEVER be asserted; (2) legitimate non-markdown assets (the templates/*.sh pattern agent-browser ships) are real content whose equality is an accepted residual, out of tech-debt: extend mirror-equality guard to all root skill artifacts (sub-docs + references) #384's scope by explicit story decision (Edge Cases exclude non-markdown) — a byte-equality check for opaque assets is a different, simpler guard. The docblock states it as a decision, not an accident.
  • Module size: 430 lines / 12 exports (round-2 skillCopySyncOptions + docs), summary still coherent (below the DR-1 recognition threshold). If it grows again, the seam to extract is lineEditScript/diffSkillMd.

Quality Assurance

Code Quality Checklist

Risk Assessment

Classification carried from refinement: risk:yellow (KB-default service criticality) · cost:green (test-only, one module + its test, no runtime path).

Risk Impact Probability Mitigation
Path-mapping assumption silently drops a nested artifact from the assertion Medium Low producedPaths cross-check asserts installedArtifactPath's derivation against the pipeline's real output set-for-set
Guard becomes count-coupled again as PR #387 adds 3 more sub-docs Low Medium datasetSkillArtifacts derives the case list from the dataset tree at collection time; explicitly tested to require no count assertion
Merge-order interaction with the 6 parked PRs (#386-391), all of which touch skills/mirrors Low Low File-disjoint from all of them; they were regenerated with the real pipeline so are expected to satisfy the stricter guard — flagged for the merge decision, not blocking here

Reviewer Guide

Review Focus Areas

  1. Confirm buildInstalledArtifacts drives the real production pipeline (no parallel transform logic) and that producedPaths genuinely cross-checks installedArtifactPath's derivation rather than trusting it.
  2. Confirm the drift-injection suite covers sub-docs AND a nested references/* file (not just SKILL.md), across every drift class.
  3. Confirm T-6's "no drift found" claim: primarily the guard passing clean against all 4 real sub-docs on main (that is the evidence). The git log trace in the T-6 table is history only — root reconciled by 7bc2f50c (fix: make pair update self-consistent for the dogfooded root install #346, root side only), dataset unchanged since 66cbfc44 ([#313] refactor: progressive disclosure for implement/review/bootstrap (task T5) #331).

Testing the Changes

git checkout chore/US-384-mirror-guard-all-skill-artifacts
pnpm --filter @pair/knowledge-hub test

Dependencies & Related Work

Related PRs

rucka added 2 commits July 31, 2026 09:20
…artifact

T-1..T-5. Guard was SKILL.md-only (#352); now data-driven over every markdown
artifact a dataset skill dir contributes — sub-docs today, references/ tomorrow.

- datasetSkillArtifacts: recursive, markdown-only, dataset-derived, no count
- installedArtifactPath: root location via the REAL transformPath, cross-checked
  set-for-set against the pipeline's actual output paths (a path-mapping wrong
  guess can no longer silently drop an artifact from the assertion)
- buildInstalledArtifacts: one real copy-pipeline run -> byDatasetPath +
  producedPaths; replaces buildInstalledSkillMd
- assertRootArtifactMatches: names the artifact by DATASET path + generated root
  path + compact diff; missing root copy points at 'pair update'
- drift-injection on non-SKILL.md artifacts: content, sibling self-pointer,
  link-depth, /command reference, missing file — each proven RED then green

Refs: #384
@rucka

rucka commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator Author

Verdict

risk:yellow · cost:greenTECH-DEBT — all 5 AC independently verified (incl. mutation-injected drift on real artifacts); no blocking issue, 1 latent pipeline defect deferred to #407 + 9 minor/test-quality items.

PR: [#406] · Author: rucka · Reviewer: independent reviewer agent · Date: 2026-07-31 · Story: [US-384] · Type: chore (test tooling / refactor)

Classification matrix — per dimension
Dimension Tier Source Note
Service/domain criticality yellow KB default tech/risk-matrix.md declares no Criticality Table ⇒ Medium default (D21)
Change/diff risk green diff footprint 2 files, +396/−89, one test-tooling module + its colocated test; no runtime/product path
Business impact yellow subdomain class Integration & Process Standardization (Supporting) — owns the dataset→root mirror
Security relevance green /assess-security-equivalent read local FS reads of repo-owned content only; no input handling, no authz, no secrets
Coupling balance green /assess-coupling-equivalent read no new integration; deliberately tightens the existing dataset↔root assertion via the real pipeline

Tier = max(assessed) = yellow — confirms the refinement-time tier (raise-only floor, D17; no change ⇒ no drift note). Cost = highest detected signal = green.

Assessments

Security — Input validation

Verdict: green — no external input: the only "inputs" are repo-owned dataset paths/contents read via readFileSync/existsSync under fixed repo-relative roots.

Details
  • readSkillsDatasetFromDisk(DATASET_SKILLS) and rootMirrorContent() resolve inside packages/knowledge-hub/dataset/.skills and .claude/skills (constants derived from __dirname).
  • installedArtifactPath() derives paths from dataset filenames — inside the repo trust boundary; no user/network input reaches it.
  • No exec/spawn, no network, no deserialization of untrusted data. The one JSON.parse targets apps/pair-cli/config.json (repo-owned).

Security — Output handling

Verdict: green — output is test-assertion text (error messages + a line diff); no rendering/serialization surface.

Security — Authentication

Verdict: green — no authentication surface touched.

Security — Authorization

Verdict: green — no access-control surface touched.

Security — Introduced vulnerabilities

Verdict: green — 0 introduced, 0 pre-existing.

Details

No new dependency, no secret material, no privileged operation. The module runs an in-memory clone of the copy pipeline (InMemoryFileSystemService) — nothing is written to disk by the guard.

Cost

Verdict: cost:green — test-only; measured CI delta ≈ +0.35s in one package.

Details
Signal Class Provider Note
CI compute green GitHub Actions skill-md-mirror.test.ts 54 → 77 cases; file runtime 660ms; suite 541 tests green
Runtime/infra spend green n/a no product runtime path touched, no new service/dependency/storage

Architecture (Coupling)

Verdict: green — no new integration; the intentional coupling to copyDirectoryWithTransforms is the design premise (assert via the real transform, never a re-implementation).

Details
  • Integration strength: unchanged (module already depended on @pair/content-ops); the new code adds no second edge.
  • Socio-technical distance: zero — same repo, same maintainer, same package boundary.
  • Volatility: the copy pipeline is moderately volatile, and that is exactly what the guard is meant to track — a pipeline change surfacing as a guard failure is the intended balance, not an imbalance.
  • One pre-existing note (not introduced here): a src/tools/ module imports InMemoryFileSystemService, exported from content-ops' test-utils — inherited from tech-debt: extend mirror-equality guard to per-skill SKILL.md pairs (all 36 skills) #352, unchanged by this PR.

Details

Findings by severity

Critical (must fix before merge)

  • none.

Major (should fix before merge)

  • packages/knowledge-hub/src/tools/skill-md-mirror.ts:137-155Deferred to tech-debt: pair update mis-installs a skill's nested references/ subdir (flattened out of the skill, both links broken) #407 (own story — not fixable here). The derivation faithfully encodes that the real pipeline flattens a nested references/ sub-dir out of its skill: process/review/references/deep.mdpair-process-review-references/deep.md. Reproduced against the real pipeline, that install is broken twice over: the SKILL.md link is rewritten to ./references/deep.md (nothing there) and the sub-doc's back-link to ../../../.skills/process/review/SKILL.md (dataset layout, not the install target). Impact: the first skill to use references/* progressive disclosure installs broken, and this guard will then assert the broken layout as the expected mirror while telling the developer to run 'pair update' — i.e. it cements the defect. The guard itself is correct-by-construction (mirroring the real transform is the point), so "fixing" it here would be wrong; the fix belongs to flatten semantics in @pair/content-ops, shared by every asset registry. Filed as tech-debt: pair update mis-installs a skill's nested references/ subdir (flattened out of the skill, both links broken) #407. Recommendation: none in this PR beyond the Minor pointer below; merge order does not depend on it (no references/ dir exists today).

Minor (consider)

Questions

  • skill-md-mirror.ts:19-27 (directionality scope) — orphan sub-docs. The guard stays directional (AC5) and pair update copies with behavior overwrite (no mirror-delete), so a sub-doc deleted from the dataset but left in .claude/skills/<skill>/ is neither asserted nor cleaned. Before tech-debt: extend mirror-equality guard to all root skill artifacts (sub-docs + references) #384 that only concerned whole root-only skills (agent-browser); now that sub-docs are part of a skill's disclosed content, a stale orphan keeps being read by agents after it no longer exists upstream. Is that an accepted residual, or worth an orphan check restricted to dataset-backed skill dirs (feasible: root .md under installedSkillDir(datasetDir) with no dataset source, which keeps agent-browser untouched)? Recommendation: decide and record it — a SCOPE note in the module docblock, or a follow-up story.
  • skill-md-mirror.ts:131-135 (markdown-only scope) — the invariant is "every markdown artifact", while the story title says "all root skill artifacts". The docblock justifies the filter with "a stray .DS_Store or a diagram" — but a legitimate non-markdown asset (the templates/*.sh pattern agent-browser already ships at root) would drift silently forever. Story-sanctioned today (Edge Cases exclude non-markdown), so not a gap to close here. Recommendation: split the rationale in the comment — junk (never assert) vs. legitimate binary/script assets (residual, out of scope) — so the boundary is a decision, not an accident.
Positive feedback
  • The cross-check is the right defense, and it works. producedPaths vs artifacts.map(installedArtifactPath) (test:81-84) closes the exact hole that would otherwise make this widening dangerous: a wrong path assumption dropping artifacts silently. Verified end-to-end — I added packages/knowledge-hub/dataset/.skills/process/review/references/deep.md and the suite grew from 77 to 78 cases with zero test edits, failing with Root mirror missing for dataset artifact 'process/review/references/deep.md': .claude/skills/pair-process-review-references/deep.md does not exist. Run 'pair update' …. AC3 proven, not asserted.
  • Mutation-verified guard, not a decoration. Appending one line to .claude/skills/pair-process-review/merge-and-cascade.md failed exactly one case, naming the artifact by dataset path, giving the generated root path, the pair update hint and the compact -/+ diff (fully visible in vitest's Received block — the message is not truncated in CI output). Deleting .claude/skills/pair-process-bootstrap/assess-orchestration.md produced the missing-copy failure. AC2 + AC4 confirmed against real artifacts, not only synthetic fixtures.
  • AC5 confirmed empirically: a real root-only scratch.md inside the dataset-backed pair-process-review/ dir left the suite green (77/77) — directionality holds in practice, whatever the test's own fixture gap.
  • T-6's "nothing to reconcile" holds up. All 45 real artifacts pass at the PR head, and git log confirms the root copy of assess-orchestration.md was last touched by 7bc2f50c (fix: make pair update self-consistent for the dogfooded root install #346) — matching the PR's claim, so a reviewer does not go hunting for a reconciliation commit.
  • Data-driven with no counts anywhere, SKILL_COPY_OPTS still pinned to apps/pair-cli/config.json, assertion helper still in the tested production module so guard and drift-injection share one code path, and the rename buildInstalledSkillMdbuildInstalledArtifacts / assertRootSkillMdMatchesassertRootArtifactMatches leaves no stale consumer (grepped repo-wide: none).
Functionality & requirements (AC coverage)
  • AC1 — every dataset artifact asserted byte-for-byte via the real transform: it.each(datasetSkillArtifacts(tree)) → 45 cases (41 SKILL.md + the 4 sub-docs), all green.
  • AC2 — drift names the artifact by dataset path + compact diffSkillMd: verified by injecting real drift (message shown in full by vitest).
  • AC3 — data-driven, no count: verified by adding a real nested dataset artifact → 78th case, no test edit.
  • AC4 — missing root copy fails loudly pointing at pair update: verified by deleting a real root sub-doc.
  • AC5 — root-only files never asserted: verified with a real extra file in a dataset-backed dir (suite stayed green); note the test's own fixture for this case is vacuous (Minor above).
  • Business rules — no parallel transform re-implementation (one copyDirectoryWithTransforms run); SKILL_COPY_OPTS pinned to the registry; no count assertions.
  • Edge cases — non-markdown excluded; SKILL.md-only dirs contribute no vacuous case; nested dirs enumerated recursively. Unreadable-file edge case only partially honoured (Minor above).
  • Error handling — appropriate (loud throw with path + hint); one mislabelled cause (Minor above).
Testing & quality gates

Run independently in a detached worktree pinned to the PR head (2c1dd33f), after pnpm install + content-ops build:

  • pnpm --filter @pair/knowledge-hub test541 passed / 541 (22 files) — matches the PR's claim exactly; skill-md-mirror.test.ts77 passed (baseline on origin/main: 54).
  • lint clean · ts:check clean · prettier:check clean.
  • jscpd packages/knowledge-hub/src/tools → 0 clones (note: the walkMarkdownFiles duplication above is cross-package, below jscpd's window).
  • pnpm skills:conformance → PASS (40 skills) · pnpm hygiene:check → PASS.
  • Mutation testing (mine, reverted): content drift → fails; missing root copy → fails; new nested dataset artifact → new case appears and fails. Gates: PASS.
  • Not re-run by me (repo-wide, unaffected by a 2-file test-tooling diff): docs:staleness, full turbo fan-out.
Adoption compliance
  • Degradation level: 1 — adoption files present and read directly (tech-stack.md, architecture.md, risk-matrix.md, way-of-working.md).
  • Dependencies: none added; @pair/content-ops and vitest are already adopted — no tech-stack.md delta needed.
  • Patterns: consistent with the "gate & tooling code in tested modules" ADL (the assertion helper stays in the production module) and with tech-debt: extend mirror-equality guard to per-skill SKILL.md pairs (all 36 skills) #352's structure, generalized.
  • ADRs: no new technical decision — a rename + widened scope of an existing guard. No missing ADR ⇒ no HALT.
  • One deviation: the projected risk:yellow tag is missing on story and PR (Minor above).
Tech debt
Documentation
Performance & deployment
  • No hot path touched (test tooling only). Guard cost: one in-memory pipeline run per suite (~660ms for the file, 45 it.each cases at ~0ms each).
  • Deployment/rollback: n/a — no shipped artifact changes; packages/knowledge-hub is not a deployable service. Revert is a plain revert of 2 files.
  • Merge-order note (from the PR, confirmed as real but non-blocking): whichever of [US-277] feat: .claude-plugin marketplace + plugin manifests with dataset catalog guard #386-391 merges after this one must satisfy the stricter guard; this branch is file-disjoint from all of them, and the widened invariant only asserts what pair update produces.

@rucka rucka self-assigned this Jul 31, 2026
rucka added 2 commits July 31, 2026 10:28
…-ops walk, #407 pointers

- non-vacuous AC5: stray root file really written into the pipeline dest
  (`InstalledMirror.root` handle) instead of asserted-absent with no fixture;
  `.DS_Store` proven copied via `root.has` instead of a tautology.
- root-copy read: EXISTS-but-unreadable rethrown with path + cause, no longer
  mislabelled "does not exist / run pair update"; catch branch now covered.
- reuse `walkMarkdownFiles` from content-ops (drop local re-walk); `posix.dirname`
  /`posix.basename` instead of hand-rolled slicing + dual path imports.
- nested-flatten mapping labelled a known defect (#407) in docblock + test names.
- AC labels normalized to #384 numbering; orphan + non-markdown residuals recorded
  as explicit decisions in the module docblock.

78 tests in skill-md-mirror.test.ts (542 in the package), lint + ts:check clean.
Formatting-only follow-up to 406f8a6 (walkMarkdownFiles reuse + stray-blank
line): prettier's arrow-chain wrap in `producedMarkdownPaths`, no behavior
change. 78 tests in skill-md-mirror.test.ts still green.
…ntradictory comments

- pin registry `behavior` via new `skillCopySyncOptions()` (resolved options the
  pipeline actually runs); a flip to 'mirror' now fails the pin test instead of
  silently invalidating the orphan residual
- import comment: `sep` also used by `producedMarkdownPaths`, not only the dataset walk
- #407 caveat: derivation goes STALE (pipeline placement changes, not transformPath);
  the produced-paths cross-check fails loudly — no "automatic" self-correction
- `rootMirrorContent`: docblock states why the on-disk read stays test-side while
  the assertion helper lives in the module

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@rucka rucka added the tech-debt Tracked technical debt (living backlog, R7.2 — never blocks a PR) label Jul 31, 2026
@rucka

rucka commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator Author

Remediation report — PR #406 · story #384

Closes the loop on the first review above. 12 findings raised (1 Major · 9 Minor · 2 Questions) → 0 open actionable findings. Two fix rounds landed in 406f8a69 and 4b230cbf (plus 8f43e630, formatting-only).

Head at verification: 4b230cbf · pnpm --filter @pair/knowledge-hub test542 passed (22 files), 78 in skill-md-mirror.test.ts · lint clean · CI green.

Each item below was re-verified against the head, not against the fix commit messages.

Major

Finding Esito Verifica
skill-md-mirror.ts:137-155 — the derivation encodes a real pipeline defect: a nested references/ sub-dir is flattened out of its skill, breaking both the SKILL.md link and the sub-doc back-link Deferred to #407 — accepted, not fixed Correct disposition: fixing it here would be wrong. The guard mirrors the real transform by construction, so making it assert a layout the pipeline does not produce would break the guard's whole premise. The defect belongs to flatten semantics in @pair/content-ops, shared by every asset registry. Marked as a known defect in the docblock and in test names (#407 appears 3× in the module, 4× in the test). No references/ dir exists today, so nothing installs broken yet — but the guard would otherwise have cemented the defect as expected behaviour.

Minor — all 9 resolved

# Finding Verifica sull'head
1 nested-flatten presented as intended behaviour with no hint it is a defect #407 now referenced in docblock + test names (3× / 4×)
2 two assertions cannot failproducedPaths.some(p => p.includes('.DS_Store')) is tautological because producedPaths is built by the markdown collector tautology gone (0 occurrences); .DS_Store now proven copied via the real pipeline handle root.has
3 collectMarkdownUnder re-implements walkMarkdownFiles, already exported by @pair/content-ops collectMarkdownUnder removed (0 occurrences); walkMarkdownFiles reused (4×)
4 rootMirrorContent collapses unreadable into undefined, so an existing-but-unreadable copy reports "does not exist. Run pair update" — wrong cause two distinct failures now: the absent path keeps its message, the unreadable path rethrows unreadable: … Fix the file/permissions — 'pair update' cannot regenerate over an unreadable path, with a dedicated describe('root-copy read distinguishes a missing copy from an unreadable one')
5 AC labels mixed across two stories in one file (#352's numbering alongside #384's) (AC2/AC3) gone; labels normalized to #384 numbering
6 neither story #384 nor PR #406 carries the risk:* tag the adoption's Tag Projection declares Active both now carry risk:yellow (+ tech-debt)
7 PR description claimed "77 … up from #352's 47"; the measured baseline on origin/main is 54, so the delta is +23, not +30 description corrected to state the measured baseline explicitly: 54 → 78 (+24). Re-measured independently: main = 14 static cases + it.each over 40 skill dirs = 54; this head = 34 static + it.each over 44 artifacts (40 SKILL.md + 4 sub-docs) = 78 executed, confirmed by vitest. The corrected figures hold.
8 hand-rolled basename arithmetic where basename from the already-imported path/posix is equivalent slicing gone, basename used. The one surviving dir === '.' (:210) is the root-vs-nested branch, a different and legitimate conditional — not the arithmetic that was flagged
9 353 lines / 11 exports — explicitly not a DR-1 violation, suggestion only acknowledged as a suggestion; no split. The recognition threshold (15+ exports / incoherent summary) is not met and the module's summary is coherent

Questions — both answered in the module

  • Directionality scope — the guard is one-way by design: pair update copies dataset→root with behavior overwrite, so a root-only stray is invisible to it. Detecting that would require a non-directional check; the limit is now stated in the docblock (:33-36) rather than left implicit.
  • Markdown-only scope — non-markdown assets (the templates/*.sh pattern) are excluded by decision, recorded as such (:169-172: "decision (Edge Cases exclude non-markdown), not an oversight"), so a future reader does not read the gap as a bug.

Beyond the findings

Round 2 added a hardening the review did not ask for: skillCopySyncOptions() pins the registry's resolved behavior, so flipping it to mirror now fails a test instead of silently invalidating the orphan-residual reasoning the module depends on.

Ancora aperto, dichiarato

Verdetto invariato: TECH-DEBT — nothing blocking; the one Major is accepted with its disposition and tracked as its own story.

@rucka
rucka merged commit e1811e5 into main Jul 31, 2026
2 checks passed
rucka added a commit that referenced this pull request Jul 31, 2026
…transform

`main` went red the moment #387 and #406 were both on it, and neither PR was
wrong. #387 (brainstorm's new `resume.md` / `parametrization.md` siblings) was
green before #406 widened the mirror-equality guard beyond `SKILL.md`; #406 was
green before those siblings existed. Merging them in EITHER order produces this
failure, so no single PR's CI could have caught it.

The cause is mine: I hand-ported those siblings to the root mirror in #387
instead of regenerating them through the real transform.

- `parametrization.md` — a missing blank line before `## $domain-placed`.
- `resume.md` — SUBSTANTIVE, not cosmetic. The hand-port applied the
  skill-reference rewrite to a FILE NAME, shipping the handoff key as
  `.pair/working/pair-process-brainstorm-<root-id | theme-slug>.md` where the
  dataset says `.pair/working/brainstorm-<root-id | theme-slug>.md`. The real
  transform does not rewrite it — it is a working-file name, not a skill
  reference — so the shipped mirror documented a different handoff key than its
  authoring source. A resume reading one and a write using the other never meet.
- `SKILL.md` + the dataset twin — emphasis style. Surfaced only while fixing the
  above, and its cause is the defect #394 fixes: the gate's `mdlint:fix` runs in
  WRITE mode, so my own gate run rewrote the DATASET's emphasis markers, leaving
  the mirror behind. The manual gate passed because it was reformatting as it
  ran; the pre-push hook then compared the rewritten dataset against the
  untouched mirror. Kept the gate's formatting (it is this repo's authority) and
  realigned the mirror.

Third time today a hand-ported mirror drifted from the transform (#408 by one
`./`, #411 likewise). The lesson is applied on the open branches: regenerate,
never hand-port.

`pnpm quality-gate` green — knowledge-hub 715 tests, mirror guard 82.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
rucka added a commit that referenced this pull request Jul 31, 2026
…y it can prove

Finishes the rebase onto the merged #406. Two fixture properties turned out to
need OPPOSITE entry depths, which is why a single fixture could not carry both.

- The **link-depth rewrite** is only observable with a ONE-segment entry:
  `demo/` sits 2 levels below the dataset root while `pair-demo/` sits 3 below
  the mirror root, so the link up must be recomputed. With a two-segment entry
  both sides are 3 levels and the rewriter correctly leaves the link untouched —
  a degenerate case that proves nothing. I moved this fixture to two segments
  earlier in the rebase and silently destroyed the assertion's purpose; it is
  back on one segment, with the reason stated in the test.
- The **nested `references/`** case needs a TWO-segment entry, because a
  one-segment entry owning a sub-directory is refused outright by the
  shallow-entry guard — and an entry deeper than the flatten depth by the
  guard added in round 3. So it gets its own `catalog/nested` entry.

Also inverted the two assertions that pinned the OLD flatten mapping. They said
so by name — "as the pipeline does today (#407)" — and were written knowing this
PR would invert them: the nested sub-dir is now expected INSIDE its skill, with
the sibling shape asserted unreachable.

The rebase conflict itself was in the same file: #406 added `behavior` to the
registry-pin test while this PR added `flattenDepth`. Both kept.

`pnpm quality-gate` green — mirror suite 78, content-ops 649.

Refs #407
rucka added a commit that referenced this pull request Jul 31, 2026
… landing

Third and fourth instance today of the same class: two PRs each green on their
own branch, red once both are on main, because each PR's guard only ever saw its
own branch. Neither PR was wrong either time.

1. **#388 + #389.** #389's AC4 audit forbids a skill doc from reading a PR "from
   the PM tool". #388's `assess-cost` report mode reads the window's merged PRs
   "from the PM tool / code host" — correctly routed, with a pointer to the
   resolution convention, so it is a FALSE POSITIVE of the guard: the pattern
   tempers itself against `code host` only in the 60 characters BEFORE
   "the PM tool", and here it comes after. Reordered to "from the code host / PM
   tool", which is also the more accurate reading — a PR is a code-host object,
   so naming that side first is right independently of the guard.

   Recorded for whoever tunes that pattern next: the temper is order-dependent,
   so `PRs … from the PM tool … (code host)` would still pass. Not widened here —
   the guard belongs to #236 and this is a main-fix, not a scope change.

2. **#408 + #406.** The bootstrap sibling's mirror shipped `[SKILL.md](SKILL.md)`
   where the real pipeline emits `[SKILL.md](./SKILL.md)` — it prepends `./` to a
   same-dir link when the file moves. #408's own sibling-parity test tolerates
   exactly that shape (it calls `neutralizeSameDirDotSlash` on both sides), while
   #406's widened mirror guard does not. So the escape hatch in one test is what
   let it reach main, where the other guard caught it.

Both fixes are one line. What is not one line is the pattern: four times today a
merge of two green PRs produced a red main. The common cause is that every guard
runs against its own branch, so an interaction is only ever observable after the
second merge — and nothing runs the full gate on the post-merge state before it
is pushed.

`pnpm quality-gate` green — 22 + 12 tasks.
rucka added a commit that referenced this pull request Jul 31, 2026
…ed flatten + link rebase + config wiring (#411)

* [US-407] fix: bound the flatten depth so a skill's nested references/ installs inside it

Test-first, per the project's bug workflow: the failing cases were written and
verified RED (`process-review-references` instead of `process-review/references`)
before any code changed.

`flattenPath` replaced EVERY separator with a hyphen, so a skill's nested dir
became a sibling pseudo-skill: `process/review/references/deep.md` installed at
`pair-process-review-references/deep.md`. The first skill to use the standard
Agent-Skills `references/` progressive-disclosure layout would install unusable.

`flattenDepth` bounds flattening to the registry's ENTRY granularity: the first
N segments are joined, deeper ones stay a real sub-path. The skills registry's
entries are two segments (`process/review`), so a third is content OF that skill.
Omitted ⇒ every separator is flattened, exactly as before — no other registry
changes behaviour, which is why this is not a blanket fix to flattenPath.

Threaded through SyncOptions -> TransformOpts -> transformPath, so the copy
pipeline honours it rather than only the pure path function.

- 8 unit cases on flattenPath/transformPath, including a REGRESSION WITNESS that
  pins the old unbounded behaviour (a bounded-depth bug must not silently become
  the default for everyone else)
- 1 end-to-end case through copyPathOps: the nested dir lands inside the skill,
  NOT as a sibling, and the skill's forward link needs no rewrite because both
  files move together

HALF THE STORY, AND IT SAYS SO. #407's AC asks for working links in BOTH
directions. The sub-doc's back-link `../SKILL.md` is still mangled to
`../../../source/process/review/SKILL.md`: `rebaseWithinMovedDir` only rebases
targets INSIDE the file's own directory, and `../SKILL.md` points at the PARENT,
so it falls through to the source-root fallback. Fixing it means anchoring the
rebase at the moved ENTRY rather than at the file's directory — a signature
change to the rewriter and its callers, deliberately not rushed. Recorded as an
`it.todo` naming the exact cause, not left as a silent gap.

`pnpm quality-gate` green. 641 tests in content-ops, 49 files.

Refs #407

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* [US-407] chore: drop two unrelated prettier reformats from the diff

The gate's prettier:fix rewrote apps/pair-cli/src/commands/kb-info/version-check*.test.ts
— pre-existing drift on main, nothing to do with this story — and `git add -A`
caught them. Reverted to origin/main: that drift belongs to #394 (pre-push gate
should run the formatters in write-mode), not to a flatten fix.

Refs #407

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* [US-407] fix: rebase a link through any directory the same copy moved (link half)

Completes #407. Test-first: both cases were written and verified RED before the
rewriter changed.

`rebaseWithinMovedDir` only rebased targets INSIDE the file's own directory. A
sub-doc's link UP to its skill (`../SKILL.md`) points at the PARENT, so it fell
through to the source-root fallback and came out as
`../../../source/process/review/SKILL.md` — a path back into the dataset layout,
dead in an install.

`movedDirs` carries every directory this copy moved, built once per batch and
threaded rewriteLinksAfterTransform -> rewriteLinksInFile -> computeNewHref ->
resolveAbsoluteTarget. A target outside the file's own directory is now rebased
through whichever moved directory contains it, most specific first. Absent, the
behaviour is unchanged.

THE DEFECT WAS NOT LIMITED TO NESTED references/. The second test proves it on
the UNBOUNDED flatten that ships today: a sub-doc becoming a sibling had its
back-link re-rooted into `source/` too. #407 described a latent defect; it is
partly a live one.

Live instance found by the mirror-equality guard, which flagged a skill I never
touched: `.claude/skills/pair-process-plan-epics/SKILL.md` shipped
`[map-subdomains](../../../.skills/capability/map-subdomains/SKILL.md)` — a
`.skills/` path that does not exist in an installed project. The corrected
transform emits `../pair-capability-map-subdomains/SKILL.md`, pointing at the
installed sibling. Regenerated: one line, one file.

`computeNewHref` exceeded the complexity ceiling once `movedDirs` was threaded;
split via `splitRewritableHref` rather than raising the limit. Caught by the
gate, not by vitest — which transpiles without type-checking, and had also let
an un-extended inline param type through as green.

`pnpm quality-gate` green. content-ops 644 tests / 49 files; the mirror guard 54.

Closes #407

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* [US-407] fix: a nested content dir is content, not a skill (review round 1)

Bounded flatten made a skill's `references/` a real sub-path, so it reached
`buildSkillNameMap`/`buildSkillLinkPathMap`/the frontmatter sync as if it were a
skill dir: `references` got registered as a SKILL NAME mapped to one arbitrary
skill's sub-dir (last writer wins on dir iteration order), and the `/references`
token in an UNRELATED skill's body was rewritten to it. Test-first: 4 cases
verified RED first.

`isRegistryEntryPath` is the one predicate — flattenDepth IS the entry
granularity, so anything deeper is content. Absent depth ⇒ every dir is an entry,
the pre-#407 behaviour unchanged.

Also, per review:
- `flattenPath` throws on a non-positive-integer depth (0/-1/1.5 used to degrade
  silently, in the direction that reintroduces the bug) and refuses a `.`/`..`
  segment in the preserved tail (`process/review/../../../../etc` joined onto the
  dest root resolved to `/etc`; the unbounded form was traversal-safe by
  construction).
- one name for the concept: `maxDepth` -> `flattenDepth` everywhere.
- `buildSkillNameMap`/`buildSkillLinkPathMap` take the full `TransformOpts` (the
  narrow declared type lied about reading `flattenDepth`); `TransformOpts` now
  lives next to `transformPath` and copy-types re-exports it.
- `rebaseWithinMovedDirs` picks the longest match in one pass instead of
  re-sorting per link; stale `rebaseWithinMovedDir` scope paragraph rewritten for
  the two-stage resolution; misplaced `computeNewHref` JSDoc moved back onto it.

Refs #407

* [US-407] fix: wire flattenDepth from config to the copy pipeline (review round 1)

The fix was unreachable for the product: `normalizeRegistryConfig` never read
`raw['flattenDepth']`, `RegistryConfig` never declared it, `buildCopyOptions`
never set it — so update/install/package always ran unbounded and a nested
`references/` still installed as the sibling `pair-process-review-references/`.
`"flattenDepth": 2` in config.json was silently dropped.

- resolver: `RegistryConfig.flattenDepth`, carried through uncoerced.
- validation: `validateFlattenDepthField` — positive integer, and rejected
  without `flatten: true`. A JSON typo now fails loudly instead of degrading back
  into the defect.
- operations: `buildCopyOptions` forwards it.
- config.json: `flattenDepth: 2` on the `skills` registry (its entries are
  `<category>/<name>`).
- skill-md-mirror: `SKILL_COPY_OPTS` + module docstring re-pointed at the
  corrected mapping (the story lists this as in scope); the pin test now asserts
  flattenDepth against config.json too. Unchanged output for every current skill
  — no dataset skill dir is deeper than two segments.
- skills-guide-mirror: reuses the single `SKILL_COPY_OPTS` instead of re-typing
  the knobs, so its `collectSkillDirs` (ANY dir holding a file) cannot register a
  `references/` dir as a skill.

CLI-level coverage through `pair update`: nested dir installs inside the skill
with both links intact; without the option the same dataset still produces the
pre-#407 sibling layout; a `flattenDepth: 0` config is rejected.

Refs #407

* [US-407] docs: ADR-020 for the bounded-flatten decision + authoring convention

The story flags this as ADR-worthy and CLAUDE.md makes it a HALT condition: the
reasoning lived only in the PR body and code comments while ADR-005 still
documents flatten as FULL flattening because "AI tools expect skills in flat
directory structures".

ADR-020 records the numeric-depth form chosen and, on the record, the three
alternatives rejected: registry-wide depth-1 (wrong for a two-segment entry),
`preserveNested: true` (a flag that still has to infer the entry), and "the dir
containing SKILL.md is the entry" — the semantically sharpest option, rejected on
COUPLING: it would leak skills-domain knowledge into a transform four non-skill
registries share, and make the installed path depend on file discovery rather
than declared config. Also records the opt-in default, the duplicated-number
trade-off, and the entry-vs-content corollary.

ADR-005 amended in place (status, Key Design Choice #2, the Flatten/prefix
rationale bullet, the config listing) so it no longer reads as authority for the
old semantic.

DoD's documentation item: `nested-sub-documents.md` states the authoring
convention — a skill may ship a nested `references/`, how it installs, that
relative links work in both directions, and that a sub-doc is content (no prefix,
no `name:` sync, not invocable). Written generic/portable per that directory's
Scope note, so it cites no ADR or issue back; dataset source + root mirror.

Closes #407

* [US-407] fix: a registry-root skill cannot ship a sub-dir — fail loudly (review round 2)

flattenDepth assumes every entry sits at the same depth; `.skills/next` is a
ONE-segment entry beside 39 two-segment ones, so `next/references` had the shape
of a real entry and would install as the sibling `pair-next-references` — the
whole defect back for that skill, blast radius including AGENTS.md via the
skill-name map. Unrepresentable, so copyDirectoryWithTransforms now rejects the
shape (a dir shallower than flattenDepth that holds files directly AND owns a
sub-dir) before copying anything. Category vs entry told apart by "holds files
directly" — no SKILL.md knowledge, per ADR-020's coupling argument.

Also from the review:
- link-rewriter: most-specific moved-dir match now runs BEFORE the own-dir
  rebase, so a forward link into a sub-dir that moved elsewhere is rewritten too
  (own-dir first left `./references/deep.md` dead under an unbounded flatten)
- mirror cleanup descends into a bounded entry, so a `references/` removed from
  the source stops staying installed forever; unbounded path unchanged, gated
- naming-transforms: typed IO_ERROR instead of bare Error; depth validated
  before the empty-path early return; `isValidFlattenDepth` shared with the CLI
  boundary so the two enforcement points cannot drift
- resolveAbsoluteTarget JSDoc: three stages, not two
- ADR-020 Trade-offs + nested-sub-documents.md (both copies): the exclusion

Refs: #407, PR #411

* [US-407] fix: round 3 — reject an entry DEEPER than flattenDepth, and close the depth-1 traversal hole

Two findings from the round-3 review, both real defects this PR introduced.

MAJOR — the round-2 guard was asymmetric. It rejected an entry SHALLOWER than
flattenDepth and let the deeper case through silently:
`capability/sub/foo/SKILL.md` installed at `pair-capability-sub/foo/SKILL.md` —
a pseudo-entry directory with NO SKILL.md at its root, invisible to the skill
loader. Two further defects followed silently because isRegistryEntryPath
reports false for it: the frontmatter `name:` stayed unsynced, and no entry
reached skillNameMap, leaving a `/foo` reference in an unrelated skill dangling.
Injection-verified: with the new guard disabled the test resolves instead of
rejecting, and skillNameMap comes out with ONE entry instead of two.

This was a REGRESSION, not an uncovered edge — full flattening produced a
perfectly usable `pair-capability-sub-foo/` for the same source. Neither CI gate
catches it: skills:conformance only walks <cat>/<sub>/SKILL.md so the file is
never read, and the #384 mirror guard derives the installed path from the same
transformPath, so it compares equal and passes.

`validateNoDeepEntry` uses the shape data already collected, with no SKILL.md
knowledge (ADR-020's coupling argument): a directory deeper than flattenDepth is
content IFF its nearest ancestor at that depth also holds files directly — that
ancestor is the entry owning it. So `process/review/references` still passes
(its depth-2 ancestor holds files) while `capability/sub/foo` fails
(`capability/sub` holds none). Both cases are tested.

MINOR — the traversal guard checked only the preserved tail, and the argument
that the head is safe ("every separator becomes a hyphen") fails at
flattenDepth 1, where the head is a single un-joined segment:
`flattenPath('../evil', 1)` returned `'../evil'`. The bounded form was therefore
strictly LESS traversal-safe than the unbounded one it replaces, in exactly the
dimension it advertises as hardened. Now every segment is validated, including
in the `segments.length <= flattenDepth` branch. Not reachable through the CLI
today, which is why it was a hole rather than a live bug.

Reverted a cosmetic rewording of the guard's message ("keep" for "preserve")
that broke two pre-existing tests — not worth the churn.

`pnpm quality-gate` green. content-ops 62 naming-transform cases, 28 copy cases.

Refs #407

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* [US-407] test: split the mirror fixture — one entry depth per property it can prove

Finishes the rebase onto the merged #406. Two fixture properties turned out to
need OPPOSITE entry depths, which is why a single fixture could not carry both.

- The **link-depth rewrite** is only observable with a ONE-segment entry:
  `demo/` sits 2 levels below the dataset root while `pair-demo/` sits 3 below
  the mirror root, so the link up must be recomputed. With a two-segment entry
  both sides are 3 levels and the rewriter correctly leaves the link untouched —
  a degenerate case that proves nothing. I moved this fixture to two segments
  earlier in the rebase and silently destroyed the assertion's purpose; it is
  back on one segment, with the reason stated in the test.
- The **nested `references/`** case needs a TWO-segment entry, because a
  one-segment entry owning a sub-directory is refused outright by the
  shallow-entry guard — and an entry deeper than the flatten depth by the
  guard added in round 3. So it gets its own `catalog/nested` entry.

Also inverted the two assertions that pinned the OLD flatten mapping. They said
so by name — "as the pipeline does today (#407)" — and were written knowing this
PR would invert them: the nested sub-dir is now expected INSIDE its skill, with
the sibling shape asserted unreachable.

The rebase conflict itself was in the same file: #406 added `behavior` to the
registry-pin test while this PR added `flattenDepth`. Both kept.

`pnpm quality-gate` green — mirror suite 78, content-ops 649.

Refs #407

* [US-407] fix: round 4 — public docs for flattenDepth, broader rule stated + enforced

Review round 4 on PR #411 (11 findings, all resolved, none escalated).

- website reference: flattenDepth row, bounded-flatten section, skills example
  (+ stale behavior mirror->overwrite there and in ADR-005 #5)
- guard rule documented AS ENFORCED (a category dir with a file hits it too),
  remediation names that way out, child depth no longer mis-interpolated
- buildTransformOpts drops flattenDepth when flatten is false — one source of
  truth for "bounded?", no unbounded transform + bounded classification
- traversal guard applied by SHAPE: a single segment ('..') is refused in BOTH
  forms, so bounded is neither less nor more safe than unbounded
- skills:conformance fails on a SKILL.md below the entry depth (authoring rule 1
  of nested-sub-documents was stated but unenforced)
- .pair/llms.txt regenerated (nested-sub-documents + pre-existing coupling-balance)
- nested mirror cleanup labelled forward-compat, ACCEPTED RESIDUAL in ADR-020
- layout validators extracted to copy/layout-validation.ts
- misattributed test comment moved onto its real subject; stale #407 caveats in
  skill-md-mirror corrected

Refs #407

* [US-407] docs: round 5 — say what the code enforces (remedy, mixed depth, guard asymmetry)

- too-deep error: second way out qualified as content-only; an entrypoint inside
  content installs as content, pinned by test (consumers have no conformance gate)
- website Bounded Flattening: mixed depth is workable — shallower entry aborts
  only once it owns a sub-dir; managing-ai-artifacts examples gain flattenDepth: 2
- flattenPath docstring/ADR-020: real invariant is 'each form checked where its
  own output can carry a live ..' — not equal strictness; tail check is stricter
  deliberately, do not 'restore symmetry'
- nested-sub-documents: heading + lead reworded to the enforced rule, new rule 5
  (link a sub-doc only from within its skill — cross-registry maps entrypoints only)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* [US-407] fix: round-4 review — null flattenDepth reaches validation, docs/ADR wording

- resolver: '!== undefined' so an explicit null is rejected by name instead of
  silently restoring the unbounded flatten (+ test).
- adr-020: the depth lead-in claimed every deviation aborts; neither guard fires
  on depth alone.
- skill-md-mirror docstring: drop the pre-#407 unbounded example stated in the
  present tense.
- layout-validation docstring: name the real audience of the qualified remedy.
- configuration.mdx: flattenDepth in the validation list.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk:yellow Classification: medium risk tier tech-debt Tracked technical debt (living backlog, R7.2 — never blocks a PR)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

tech-debt: extend mirror-equality guard to all root skill artifacts (sub-docs + references)

1 participant