feat(access,cli): the integrity checks, ow check, and source state - #8
Conversation
Closes plan tasks 7.1-7.5, 7.7 and 6.1. Settles the design gap the plan deferred to group 7, and the gap was wider than the note there described. `listEntityPages` read only the top level of `wiki/`, while the plan's own layout files pages as `wiki/projects/`, `wiki/people/`, `wiki/topics/`. So a project that followed the documented layout had no pages at all as far as the store was concerned: nothing indexed, no orphan reported, `ow graph` empty, MCP serving nothing, every `[[link]]` reading as broken. The gate still validated writes to them, which is what kept it quiet. A page is now its slug wherever it sits under `wiki/` — a folder is organisation, a link is a name — recorded as `adr:0016-a-page-is-its-slug-wherever-it-sits`. Slug uniqueness is the one rule that needs, and it is a finding rather than a silent choice. Codewiki lives at `wiki/codewiki/`; a top-level `codewiki/` is no longer gated and is reported as misplaced. The checks (7.1-7.5) each answer a question the gate cannot, because every one is about a relationship between things rather than a single write. 7.4 needs no glossary file: a page's `title` is the canonical term and its `aliases` are the synonyms, so a separate file would be a second record of one fact. The scaffolded skill told the agent to "check the glossary" — a file that has never existed — and now says what is actually there. 6.1 derives state from disk rather than persisting it. The filesystem is already persisted and resumable: manifest.json, text.md, the pages, journal.json. A state file beside those is the same two-records problem. `ow check` (7.7) exits 0 clean / 1 could not run / 2 found errors, matching the `scc` contract this repo documents. Only errors fail: a source uploaded this morning that nothing cites yet must not turn CI red. From the two reviews on this branch: - Catastrophic backtracking in the codewiki citation regex. `/` was in both the segment class and the separator, so the pattern was ambiguous: ~26 slash segments took 1.6s, ~34 took minutes. Eighty bytes of page body — which an agent writes, possibly steered by a poisoned source in raw/ — wedged `ow check`, CI and the UI in a synchronous spin no try/catch interrupts. - `ow graph`, `ow search` and the MCP read tools all still assumed a page was at the top level, so the ADR's central claim was false in three places. MCP's was silent: a superseded page came back as `type: unknown, status: active`. - The gate started denying any page that wrote `[[changelog]]` or `[[index]]`, with a reason that read as a bug — those files exist in every scaffolded project and the skill tells the agent to use them. - `checkProject` created `wiki/` through `readIndex`, so a read wrote — and it is exported into the read-only surface the MCP process imports. - `checkVocabulary` blamed the wrong page when two pages claimed one alias, and told a page to stop writing its own title. Both are now reported as the conflict they are. - Reported lines pointed into the frontmatter; a citation on a heading line was not counted; a page's H1 title was reported as an uncited section; a file ending in a newline was counted one line too long, so a citation one line past the end was accepted. - Control characters from page content reached the terminal unescaped, letting an alias forge `ow check: no findings`. - `checkLinks` walked the wiki once per page; 3s over 800 pages. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016iMM93Wk43o44V5J2AxPgL
📝 WalkthroughWalkthroughThe PR defines nested wiki pages by globally unique slug, adds derived source-state APIs and integrity checks, exposes them through the access package, and adds the ChangesWiki addressing and placement
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant CLI
participant AccessChecks
participant WikiStore
participant Filesystem
CLI->>AccessChecks: run check project
AccessChecks->>WikiStore: load discovered pages
WikiStore->>Filesystem: read nested wiki files
Filesystem-->>WikiStore: return page contents
WikiStore-->>AccessChecks: return loaded pages
AccessChecks-->>CLI: return findings and exit status
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (4)
packages/mcp/src/tools.ts (1)
107-125: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winCache the page list during MCP index construction
readPageFrontmatterresolves each slug throughlookupPagePath, which performs a fulllistPageswalk. Therefore,indexStructureperforms one directory walk per page and becomes O(n²). Reuse one precomputed page list, matching theknown-set pattern incheckLinks.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/mcp/src/tools.ts` around lines 107 - 125, Update indexStructure to precompute the page list once and pass or reuse it when resolving each slug through readPageFrontmatter and pagePath, avoiding repeated lookupPagePath/listPages walks. Follow the existing known-set caching pattern in checkLinks while preserving page path confinement and frontmatter behavior.packages/mcp/tests/mcp.spec.ts (1)
225-227: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueThe ternary has identical branches.
Both arms evaluate
spec.startsWith(a), so thea.endsWith(":")test decides nothing. Either drop the condition or implement the distinction the condition implies, for example an exact match for a plain package name and a prefix match only for anode:-style scheme.♻️ Proposed cleanup
- return ALLOWED.some( - (a) => spec === a || (a.endsWith(":") ? spec.startsWith(a) : spec.startsWith(a)), - ); + return ALLOWED.some((a) => spec === a || spec.startsWith(a));🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/mcp/tests/mcp.spec.ts` around lines 225 - 227, Update the ALLOWED matching logic in the visible .some callback to remove the redundant ternary and implement the intended distinction: keep exact matching for plain package names, while allowing prefix matching only for entries ending in “:”, such as node: schemes.packages/cli/tests/hooks.spec.ts (1)
40-44: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd the negative case for a top-level
codewiki/.
packages/cli/src/hooks.tsLines 104-107 removedcodewiki/from the shell-write targets so the hook and the gate agree. No test covers that removal, so a later re-add of the pattern passes.💚 Proposed test
it("flags cp/mv/tee/sed -i into wiki/, codewiki included", () => { expect(detectShellWrite(`cp /tmp/x wiki/fenix.md`, "/p")).toBe("wiki/fenix.md"); expect(detectShellWrite(`mv /tmp/x wiki/codewiki/dispatch.md`, "/p")).toBe( "wiki/codewiki/dispatch.md", ); + }); + + it("ignores a top-level codewiki/, which the gate allows (adr:0016)", () => { + expect(detectShellWrite(`echo hi > codewiki/x.md`, "/p")).toBeNull();🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/cli/tests/hooks.spec.ts` around lines 40 - 44, Add a negative assertion to the existing “flags cp/mv/tee/sed -i into wiki/, codewiki included” test around detectShellWrite, verifying that a write targeting top-level codewiki/ returns no flagged path. Keep the existing wiki/ and nested wiki/codewiki/ positive cases unchanged.packages/cli/src/commands/check.ts (1)
25-33: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winReject unknown arguments.
parseCheckArgsignores any argument it does not recognise. A typo such asow check --jsonnruns the text renderer and still exits 0 on a clean wiki. A CI job that parses stdout as JSON then fails on output it cannot explain.Return a parse error for unrecognised arguments, and let
main.tsreport it throughfail.♻️ Proposed refactor
export function parseCheckArgs(args: string[]): CheckOptions { + const known = new Set(["--json", "--errors-only"]); + const unknown = args.filter((a) => !known.has(a)); + if (unknown.length > 0) { + throw new Error(`ow check does not take ${unknown.join(", ")}`); + } return { json: args.includes("--json"),Note that
main.tsLines 91-103 already catch a throw here and returnCHECK_FAILED_TO_RUN, which is the correct code for "the check could not run".🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/cli/src/commands/check.ts` around lines 25 - 33, Update parseCheckArgs to validate every argument against the supported --json and --errors-only flags, throwing a parse error for any unrecognised argument. Preserve the existing defaults and return shape for valid arguments, allowing main.ts to catch the throw and report it through fail with CHECK_FAILED_TO_RUN.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/access/src/check/checks.ts`:
- Line 209: Apply safe() to the raw interpolated issue values in checkProvenance
and checkSchema: sanitize issue.reason in the provenance message at
packages/access/src/check/checks.ts:209-209, and sanitize both issue.field and
issue.reason at packages/access/src/check/checks.ts:536-536. Extend the
assertion in packages/access/tests/check.spec.ts:504-507 to include provenance
and schema findings, ensuring all finding messages enforce the scrubbing
invariant.
- Around line 412-443: Update the citation scan before iterating in the
page-body check to mask fenced code blocks, replacing non-newline characters
with spaces so line positions remain unchanged. Run CODEWIKI_CITATION matching
against the masked content instead of page.body, while preserving the existing
resolution and finding logic.
- Around line 505-520: Replace the recursive readdirSync call in the stray-page
check with an explicit walk matching listPages, and skip any entry where
entry.isSymbolicLink() is true before descending or counting files. Preserve
recursive discovery of regular .md files under stray while preventing symlinked
directories from contributing pages.
In `@packages/access/src/sources/state.ts`:
- Around line 116-123: Update listSourceStates to handle readManifest failures
independently for each source returned by listSources, including malformed JSON
and concurrent deletion, so one invalid manifest does not abort listing valid
states. Skip or otherwise safely exclude the failing source while preserving
sorting and the existing citations behavior for successfully loaded sources; do
not modify the unrelated ow check path.
In `@packages/access/src/store/index.ts`:
- Around line 60-90: Update the extension filter in listPages to compare a
case-folded entry.name against ".md", matching gatedPageRel’s case-insensitive
behavior; leave slug extraction and page path handling unchanged so files such
as fenix.MD are discovered.
In `@packages/access/tests/sources-state.spec.ts`:
- Around line 118-120: Add a test case alongside the existing “refuses an id
that escapes raw/” test using a single-parent traversal such as “../something”,
and assert that sourceState(root, ...) throws. Keep the existing project-root
escape coverage intact while ensuring the test specifically validates rejection
of paths outside raw/ but still within projectRoot.
---
Nitpick comments:
In `@packages/cli/src/commands/check.ts`:
- Around line 25-33: Update parseCheckArgs to validate every argument against
the supported --json and --errors-only flags, throwing a parse error for any
unrecognised argument. Preserve the existing defaults and return shape for valid
arguments, allowing main.ts to catch the throw and report it through fail with
CHECK_FAILED_TO_RUN.
In `@packages/cli/tests/hooks.spec.ts`:
- Around line 40-44: Add a negative assertion to the existing “flags
cp/mv/tee/sed -i into wiki/, codewiki included” test around detectShellWrite,
verifying that a write targeting top-level codewiki/ returns no flagged path.
Keep the existing wiki/ and nested wiki/codewiki/ positive cases unchanged.
In `@packages/mcp/src/tools.ts`:
- Around line 107-125: Update indexStructure to precompute the page list once
and pass or reuse it when resolving each slug through readPageFrontmatter and
pagePath, avoiding repeated lookupPagePath/listPages walks. Follow the existing
known-set caching pattern in checkLinks while preserving page path confinement
and frontmatter behavior.
In `@packages/mcp/tests/mcp.spec.ts`:
- Around line 225-227: Update the ALLOWED matching logic in the visible .some
callback to remove the redundant ternary and implement the intended distinction:
keep exact matching for plain package names, while allowing prefix matching only
for entries ending in “:”, such as node: schemes.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 91d15756-83f9-4d30-886f-98aa4798c085
📒 Files selected for processing (28)
docs/adr/0016-a-page-is-its-slug-wherever-it-sits.mdpackages/access/src/check/checks.tspackages/access/src/check/findings.tspackages/access/src/gate/gate.tspackages/access/src/index.tspackages/access/src/read.tspackages/access/src/skills.tspackages/access/src/sources/state.tspackages/access/src/store/index-write.tspackages/access/src/store/index.tspackages/access/src/store/page.tspackages/access/src/store/wikilinks.tspackages/access/tests/check.spec.tspackages/access/tests/gate-decision.spec.tspackages/access/tests/sources-state.spec.tspackages/access/tests/store-index.spec.tspackages/access/tests/store-wikilinks.spec.tspackages/cli/src/commands/check.tspackages/cli/src/commands/graph.tspackages/cli/src/commands/search.tspackages/cli/src/hooks.tspackages/cli/src/main.tspackages/cli/tests/check.spec.tspackages/cli/tests/hooks.spec.tspackages/cli/tests/queries.spec.tspackages/mcp/src/tools.tspackages/mcp/tests/mcp.spec.tsplans/open-wiki.md
- `listPages` folds case on `.md`, as the gate does. `gatedPageRel` lowercases before testing the extension, so it validates and accepts `wiki/fenix.MD` — and matching case-sensitively here meant that page was accepted by the gate and then invisible to the index, the orphan check, `ow graph` and MCP. That is the failure this addressing model exists to end, reappearing one level down. - A citation inside a fenced code block is an example, not a citation. A codewiki page documenting the citation form — which the skill's own prose does — failed `ow check` for its own sample. - The stray-codewiki walk no longer follows symlinked directories out of the project; `recursive: true` does. - `listSourceStates` keeps going when one manifest will not parse or a source vanishes mid-listing. A sources screen showing nothing because of one bad directory is worse than one showing the other nineteen. - `listSources` reads dirents rather than stat'ing each entry, so a dangling symlink under `raw/` no longer throws ENOENT and aborts the whole run — reachable now that `checkRecords` calls it. - `sourceState` confines against `raw/`, not merely the project: an id like `../wiki` stays inside the project and is still not a source. The test that claimed to cover this only exercised an id that left the project entirely, which proves less. - `safe()` on the remaining interpolated values, in `checkProvenance` and `checkSchema`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016iMM93Wk43o44V5J2AxPgL
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
packages/access/tests/sources-state.spec.ts (1)
149-157: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winAdd a dangling-symlink regression test.
listSourcesnow usesDirent.isDirectory()to avoidstatSyncfailures for dangling entries. Add a dangling symlink beside a valid source and assert thatlistSourceStates(root)keeps the valid source and does not throw.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/access/tests/sources-state.spec.ts` around lines 149 - 157, Add a regression test alongside the existing malformed-manifest case that creates a valid source and a dangling symlink in the root directory, then call listSourceStates(root) and assert it returns the valid source ID without throwing. Use the existing filesystem helpers and preserve the expected valid-source listing behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/access/src/sources/manifest.ts`:
- Around line 92-100: Update the source-manifest discovery logic around the
manifest listing loop to read and parse each existing manifest.json, rather than
only checking existsSync. When parsing fails, add a finding identifying the
malformed manifest and its source directory; retain valid manifest IDs and
continue checking other entries without aborting.
---
Nitpick comments:
In `@packages/access/tests/sources-state.spec.ts`:
- Around line 149-157: Add a regression test alongside the existing
malformed-manifest case that creates a valid source and a dangling symlink in
the root directory, then call listSourceStates(root) and assert it returns the
valid source ID without throwing. Use the existing filesystem helpers and
preserve the expected valid-source listing behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: ae406955-cc0f-407d-bfb6-102e416f2871
📒 Files selected for processing (6)
packages/access/src/check/checks.tspackages/access/src/sources/manifest.tspackages/access/src/sources/state.tspackages/access/src/store/index.tspackages/access/tests/check.spec.tspackages/access/tests/sources-state.spec.ts
🚧 Files skipped from review as they are similar to previous changes (3)
- packages/access/src/sources/state.ts
- packages/access/src/store/index.ts
- packages/access/src/check/checks.ts
| for (const entry of readdirSync(raw, { withFileTypes: true })) { | ||
| if (entry.name === INBOX) continue; | ||
| // `withFileTypes` describes the entry itself, so a dangling symlink is | ||
| // reported rather than stat'd. `statSync` on one throws ENOENT, which used | ||
| // to abort the whole listing — and `ow check` with it. | ||
| if (!entry.isDirectory()) continue; | ||
| const dir = join(raw, entry.name); | ||
| if (!existsSync(join(dir, "manifest.json"))) continue; | ||
| ids.push(entry); | ||
| ids.push(entry.name); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n -C 12 \
'listSources\(projectRoot\)|readManifest|JSON\.parse|catch|exitCode|process\.exit' \
packages/access/src/check/checks.ts \
packages/access/src/sources \
packages/access/tests \
packages/cli/src \
packages/cli/testsRepository: protonspy/open-wiki
Length of output: 50376
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- check implementation ---'
sed -n '1,235p' packages/access/src/check/checks.ts
printf '%s\n' '--- check result and CLI boundary ---'
sed -n '560,635p' packages/access/src/check/checks.ts
rg -n -C 14 'runCheck|checkProject|findings|sources:|exit|check' packages/cli/src packages/access/src packages/cli/tests packages/access/tests \
| head -n 500
printf '%s\n' '--- malformed-manifest coverage ---'
rg -n -C 8 'malformed|invalid|manifest\.json|listSourceStates|source\.uncited|checkProvenance' \
packages/access/tests packages/cli/testsRepository: protonspy/open-wiki
Length of output: 50377
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
checks = Path("packages/access/src/check/checks.ts").read_text()
manifest = Path("packages/access/src/sources/manifest.ts").read_text()
provenance = Path("packages/access/src/store/provenance.ts").read_text()
def section(text, start, end):
a = text.index(start)
b = text.index(end, a)
return text[a:b]
records = section(checks, "export function checkRecords", "export function checkProvenance")
project = section(checks, "export function checkProject", "}")
source_exists = section(manifest, "export function sourceExists", "/**")
resolve = section(provenance, "export function resolveProvenance", "}")
print("checkRecords calls listSources:", "listSources(projectRoot)" in records)
print("checkRecords calls readManifest:", "readManifest(" in records)
print("checkProject calls listSources:", "listSources(projectRoot)" in project)
print("sourceExists calls readManifest:", "readManifest(" in source_exists)
print("resolveProvenance calls sourceExists:", "sourceExists(" in resolve)
print("resolveProvenance calls readManifest:", "readManifest(" in resolve)
PY
printf '%s\n' '--- provenance resolver ---'
rg -n -C 18 'function resolveProvenance|sourceExists|readManifest|JSON\.parse' \
packages/access/src/store/provenance.ts packages/access/src/sources/manifest.tsRepository: protonspy/open-wiki
Length of output: 9272
Validate source manifests in ow check.
ow check only checks that manifest.json exists. It does not parse the manifest or report malformed JSON. Add a finding for each malformed manifest.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/access/src/sources/manifest.ts` around lines 92 - 100, Update the
source-manifest discovery logic around the manifest listing loop to read and
parse each existing manifest.json, rather than only checking existsSync. When
parsing fails, add a finding identifying the malformed manifest and its source
directory; retain valid manifest IDs and continue checking other entries without
aborting.
Closes plan tasks 7.1–7.5, 7.7 and 6.1 of
plans/open-wiki.md.The design gap the plan deferred here — and how much wider it was
Plan task 7.5 recorded a gap to settle in this group: the gate accepted a top-level
codewiki/while the skill's prose put codewiki underwiki/. Building the checks showed it was never about codewiki.listEntityPagesread only the top level ofwiki/. The plan's own layout diagram files pages aswiki/projects/*.md,wiki/people/*.md,wiki/topics/*.md. So a project that followed the documented layout had no pages at all as far as the store was concerned — nothing indexed, no orphan ever reported,ow graphempty, MCP serving nothing, every[[link]]reading as broken. The gate still validated writes to those pages, which is exactly what kept the contradiction quiet: they were checked on the way in and then lost their existence.Settled as
adr:0016-a-page-is-its-slug-wherever-it-sits: a page is addressed by its slug wherever it sits underwiki/. A folder is organisation; a link is a name — which is what makes[[wikilink]]work at all, and it is the only reading that leaves both the plan's diagram and the scaffolded skill true. That matters more than usual because the skill is copied into user projects and ages there. The one rule the model needs is slug uniqueness, reported aspage.duplicate-slugrather than resolved by silently picking one.What else is here
ow check, exiting0clean /1could not run /2found errors, matching thescccontract this repo already documents. Only errors fail: a source uploaded this morning that nothing cites yet must not turn CI red.Two judgement calls worth naming:
7.4 needs no glossary file. Every page already declares its canonical
titleand itsaliases; a separate glossary would be a second record of one fact, and the copy is the one that goes stale. The scaffolded skill told the agent to "check the glossary" — a file that has never existed in this product — and now describes what is actually there.6.1 derives rather than persists. "Persisted and resumable" is already what the filesystem is:
manifest.json,text.md, the pages,journal.json. A state file beside those is the same two-records problem. Recorded on the plan's 6.1 line so a reader can see it was answered differently than it was asked.From the reviews on this branch
code-reviewandsecurity-reviewwere run before this PR was opened, and between them found 17 issues — several of them regressions this change itself introduced. All are fixed:/was in both the segment class and the separator, making the pattern ambiguous: ~26 slash-separated segments took 1.6 s, ~34 took minutes. That is eighty bytes of page body — which an agent writes, possibly steered by a poisoned source inraw/— wedgingow check, CI and the future UI in a synchronous CPU spin notry/catchcan interrupt. Measured at 1602 ms → 0 ms after the fix.ow graph,ow searchand the MCP read tools all still assumed a top-level page, so the ADR's central claim was false in three places. MCP's failure was silent: a superseded page filed underwiki/topics/came back astype: unknown, status: active.pagePathhad been written for exactly this and wired to nothing.[[changelog]]or[[index]]— files that exist in every scaffolded project, and that the skill tells the agent to use — with a reason that reads as a bug.checkProjectcreatedwiki/viareadIndex, so a read wrote. It is exported into the read-only surface the MCP process imports, whose whole guarantee is that read-only is what that process can do.checkVocabularyblamed the wrong page when two pages claimed one alias, and told a page to stop writing its own title. Both are now reported as the conflict they are (glossary.conflict).ow check: no findingsline over the real report.checkLinkswalked the wiki once per page — 3 s over 800 pages, with the pages already in hand.How it was verified
pnpm run typecheck,pnpm lintclean;scc validate0 findingsNote
mainis still not prettier-clean (CI runstypecheckandlint, notformat:check); that churn is kept out of this diff.🤖 Generated with Claude Code
https://claude.ai/code/session_016iMM93Wk43o44V5J2AxPgL
Summary by CodeRabbit
ow checkcommand to identify wiki integrity issues, with text or JSON output and actionable fixes.