Commit Graph

9 Commits

Author SHA1 Message Date
Amir Fathi f268d43744 fix(lint): stop flagging embeds and normalise .md/escaped-pipe wikilink targets (#204)
* fix(lint): stop flagging embeds and normalise .md/escaped-pipe wikilink targets

broken_links harvests every [[wikilink]] target, including embeds
(![[diagram.png]], [[board.canvas]], [[dashboard.base]]) that by_slug can
never hold since _iter_pages globs only *.md. An explicit .md suffix
([[alpha.md]]) and a table-escaped pipe ([[beta\|B]]) were also compared
raw against the slugged index instead of being normalised the way
_normalise_node_id already normalises a page's own id, so both never
resolved either.

_wikilink_page_target() returns None for a non-.md-extension target
(skipping attachment embeds) and otherwise strips an explicit .md suffix
and a trailing backslash before _parse_page slugs it. On the issue's own
repro vault this drops broken_links from 7 flagged (6 false positives) to
the one genuinely broken link.

Fixes #176

* fix(lint): only skip attachment extensions, not every dotted page name

Path("Node.js").suffix is ".js", so the previous `if suffix: return None`
dropped links to any page whose name contains a dot. Those links feed orphan
detection and link_count as well as broken_links, so "Node.js", "Next.js",
"v1.2 release notes" and friends were reported as orphans.

Skip only Obsidian's attachment extensions instead.

---------

Co-authored-by: ar9av <ar9avg@gmail.com>
2026-09-03 04:09:35 -07:00
Zorion Arrillaga f085d14f04 feat(lint): report pages that share a slugged stem
`graph_analysis` identifies a page by its slugged filename stem, so two pages
whose stems slug alike are one node — and one entry in the graphrag index.
On a vault holding concepts/vector-search.md and entities/vector-search.md,
build_index keeps only the entities page; the concepts page is absent from
the index that memory_search and graph-query read.

duplicate_titles does not cover this: it keys on the title: value, and stems
can collide while titles differ.

Add duplicate_stems, built over graph_analysis.iter_pages with
graph_analysis._page_slug so the report keys as the merge does. It reads
pages rather than links, so a collision nothing references is still reported.
Severity warn, beside duplicate_titles.

Borrowing the graph's page selection means a collision inside _bootstrap/ is
now visible to this check, where lint's own SKIP_DIRS excludes that
directory. The existing bootstrap exclusion test still passes; the tradeoff
is called out in the pull request rather than settled here.
2026-08-28 01:05:20 +02:00
Arnav 8b5859d0f8 Vault state management: manifest write safety, lifecycle transitions, and equilibrium mode (#195)
* fix(cache): lock and atomically write .manifest.json

update_source was a plain read-modify-write with no mutual exclusion, so
any two overlapping writers lost an entry silently: the second reader's
snapshot predated the first writer's write. This fires in three real
configurations — batch-plan fan-out dispatching parallel ingest subagents,
the Dockerized server writing a vault a local skill is also using, and a
scheduled daily-update overlapping an interactive session.

Reproduced with 12 concurrent update_source processes against one vault:
6 of 12 entries recorded before, 12 of 12 after.

Writes now take an advisory lockfile and replace the manifest atomically
via a temp file plus os.replace. O_CREAT|O_EXCL rather than fcntl because
this ships on Windows. Hashing stays outside the lock — it can take
seconds on a large source tree and nothing in the manifest depends on it.

The contention test spawns real subprocesses; a single-process test
cannot observe this race.

* feat(trust): flag illegal lifecycle transitions in lint

The lifecycle field is a state machine, but nothing enforced its edges.
Ledger entries now record the lifecycle at review time, giving lint a
baseline to compare the current frontmatter against.

Only edges that are illegal for every possible sequence of unobserved
intermediate states are flagged: falling back to draft (only ingest sets
draft) and exiting archived (terminal). draft -> verified is deliberately
not flagged — ledger snapshots are sparse, so a legitimate intermediate
reviewed may simply never have been recorded, and flagging that pair
would fire on valid history.

Additive: entries predating the lifecycle field carry no baseline and are
skipped, so existing vaults behave identically until their next
trust-record. Owner-extended lifecycle states never match the illegal
pairs, so the check degrades to a no-op rather than firing on states it
does not understand. Warns by default, fails under --strict-trust,
matching how the surrounding trust checks were already staged.

* feat(wiki-status): add vault equilibrium mode

The maintenance skills each optimize one shared vault for a different
objective, so any of them can undo another's work and no single skill
reporting clean means maintenance is done.

Equilibrium mode runs every maintenance audit in report-only form and
reports whether any skill still has a pending change. The vault is
converged only when all of them propose nothing.

It also detects oscillation via a snapshot in _insights.md, alongside the
existing GRAPH_SNAPSHOT: a skill whose pending count keeps returning
after being driven to zero is being reversed by another skill, and the
vault can never settle. Reported, never auto-fixed — resolving it is a
decision about whose objective wins.

Pending moves are attributed back to their manifest source, so a source
that repeatedly produces pages needing repair surfaces as a suggestion to
review its source_quality. Report-only; the owner still decides through
the existing trust flow.

Skill-only, no Python: lint already emits JSON counts and the other three
audits are LLM passes.

* docs: document manifest write safety, lifecycle checks, and equilibrium

Covers the advisory lock and atomic manifest write (including the
parallel-run contract), the illegal_lifecycle_transitions check and why
draft -> verified is excluded, and equilibrium mode in the architecture
feature list.
2026-08-25 17:19:03 -07:00
Arnav ea1633a8d4 fix(tests): isolate CLI subprocess tests, and actually run the suite in CI (#174)
* fix(tests): isolate CLI subprocess tests, and run the suite in CI

12 tests failed on a clean checkout. Neither was a product bug — both were
test-harness isolation gaps, and CI never caught them because the suite was
never run there.

1. Wrong package under test. The 18 test files that spawn
   `python -m obsidian_wiki.cli` each build their own env from
   os.environ.copy() and never set PYTHONPATH, so sys.path[0] was the
   subprocess cwd. Any test running from a temp dir imported whatever
   obsidian_wiki happened to be pip-installed instead of the working tree —
   here a stale build predating `code-understand`, so all 8
   test_code_understand_cli tests died at argparse with "invalid choice",
   plus 4 in test_context_pack_cli. One autouse session fixture in
   conftest.py now pins PYTHONPATH for every helper at once.

2. Developer .env leaking in. test_lint's `_run` and test_context_pack's
   `run_cli` inherited pytest's cwd (the repo), so config resolution walked
   up and found a developer's own untracked .env, pointing the run at their
   real vault instead of the fixture. Both now run from the fake HOME.

Verified the fix isn't masking anything: sabotaging the config resolver makes
the relevant tests fail, and the suite passes in a venv with no obsidian_wiki
installed at all — so PYTHONPATH is genuinely doing the work.

Adds .github/workflows/tests.yml to run pytest on PRs and main. The workflows
only ran setup.sh, wheel publish, and the README-sync checker, so nothing
executed tests/ — the real reason this drifted.

Also drops the stale "RED by design / command does not exist yet" docstring
in test_code_understand_cli.py; the wiring landed in fd30720.

566 passed, 1 skipped (was 554 passed / 12 failed).

* fix(tests): make three test modules importable on Python 3.9

test_batch, test_graph_analysis and test_graphrag annotate helpers with
`int | None` / `list[str] | None` but lack `from __future__ import
annotations`, so 3.9 evaluates the union at import time and collection dies
with "unsupported operand type(s) for |". Every module under obsidian_wiki/
already has the future import; only these three tests were missing it.

Surfaced by the new pytest CI job — the 3.9 leg (the requires-python floor)
failed at collection while 3.12 passed.
2026-08-16 21:14:55 -07:00
dj-wishket 5ef66b6bec feat(trust): enforce owner authority and correction provenance (#162) 2026-08-05 15:56:06 -07:00
Arnav 2363445eac fix(lint): stage trust-ledger enforcement behind strict_trust (#146) (#147)
Legacy vaults fail-closed on every pre-existing page after the trust-ledger
rollout: missing base_confidence/lifecycle and a missing ledger became
mandatory failures with no opt-out, skipping the staged migration from #28.

Split confidence/lifecycle out of the core required-frontmatter check into a
separate confidence_missing_fields finding, and add a strict_trust switch to
lint_vault (default off): missing trust fields, ledger errors, stale reviews,
and score mismatches now warn by default and only fail once a vault owner
opts in via --strict-trust or OBSIDIAN_TRUST_STRICT=1. Ledger presence alone
no longer silently enables strict mode.
2026-07-23 11:43:07 -07:00
Singgih Lukman Anggana e5cd869fdb fix(trust): harden ledger validation
Reject symlinked writes and malformed or ambiguous trust data, enforce complete page coverage and strict timestamps/lifecycle values, and parse typed relationships independent of key order.
2026-07-12 19:06:44 +07:00
Singgih Lukman Anggana 6242472b8f fix(lint): add approved confidence trust ledger
Replace source-string confidence guesses with a manually approved, fingerprinted review ledger. Add exact relationship validation, fail-closed CLI checks, partial review recording, and regression coverage.
2026-07-12 18:24:38 +07:00
ar9av 87eefd8846 Add obsidian-wiki lint command 2026-07-01 22:14:08 -07:00