Commit graph

10 commits

Author SHA1 Message Date
Aaron Bockelie
8bb580cc56 fix: .mcpignore semantics, TLS 1.3 startup, rename extension (#250, #252, #253)
Bundles the three reported bugs plus an unreported under-blocking bug found
while writing the tests for #250, and adds the coverage tooling that would
have caught all four.

#250 — .mcpignore negation was inverted
The parser stripped the leading '!' before Minimatch saw it, so .negate was
always false and the branch reading it was dead code. '!folder/*' compiled
into a positive exclusion: negation did not merely fail, it inverted.

Under-blocking (found while testing the above, not reported)
Patterns were handed to Minimatch raw, so two gitignore rules were never
implemented: a slash-less pattern matches at any depth ('*.secret' must hide
'a/b/creds.secret'), and a directory match covers its contents ('private/'
must hide 'private/notes.md'). No consumer of isExcluded() checks ancestors —
every caller passes a full file path — so nothing compensated downstream. The
plugin's own shipped .mcpignore template promises both behaviours, so a user
following it believed private files were hidden while they were being served
to the model. Patterns are now translated into the globs that implement
gitignore semantics, and negation is interpreted here rather than by Minimatch.

#252 — min TLS 1.3 crashed the server
secureProtocol mapped 1.3 to 'TLSv1_3_method', which OpenSSL never shipped, so
context creation threw "Unknown method: TLSv1_3_method". Switched to minVersion,
which also fixes a second latent bug: secureProtocol PINS one version, so
"minimum TLS 1.2" was silently refusing TLS 1.3 clients. The setting now
behaves as the floor it is labelled.

#253 — rename dropped the extension
'newName' was concatenated onto the source directory verbatim, so renaming
'note.md' to 'renamed' produced an extension-less file that drops out of
markdown views. The source extension is now carried over when newName omits
one, before the overwrite guard so it checks the right path.

Coverage + test contract
All four bugs shipped through code paths with zero coverage. Adds `make
coverage` / `coverage-map` / `coverage-gate`, a ratcheting coverageThreshold
(security boundary held to a far higher floor than global, since a hole there
leaks vault content), and tests/test-contract.test.ts enforcing that a green
run means something: no .only, no .skip, every file asserts, no tautologies.
The contract immediately caught an `expect(true).toBe(true)` placeholder in the
path-validator suite, now replaced with real assertions.

BREAKING (.mcpignore, both intended):
- The double-'!' workaround for #250 now correctly parses as a double negation
  (net exclude) and will stop working.
- A bare '*' now excludes at every depth, per gitignore, rather than top-level
  only. This is what makes the '*' + '!keep/**' whitelist idiom work.
2026-07-13 17:46:50 -05:00
Aaron Bockelie
024e9cb1b5 harden(scorecard): baseline refuses STALE portal; gate notes it (#165)
Advisor follow-up. Asymmetry by design:
- scorecard-gate.mjs TOLERATES a STALE portal (comparing an old scan
  is harmless) but now prints a non-gating note so a pass/fail is read
  in context.
- scorecard-baseline.mjs REFUSES on STALE (exit 4) the way it refuses
  on drift — anchoring the baseline to an outdated scan would freeze a
  stale truth and mis-calibrate every future gate run.

Validated: gate exit 0 + STALE note when manifest>portal; baseline
exit 4 (refused). Refs #165
2026-05-16 23:43:32 -05:00
Aaron Bockelie
f1cc9d63ba feat(scorecard): scheduled regression gate vs committed baseline (#165)
Standalone weekly watch (+ workflow_dispatch) that FAILS only on a real
portal regression vs scripts/scorecard-baseline.json:
- Health/Review score-ratio downgrade
- automated issue count increase
- a new behaviour/permission ("**Title**:") finding

Never inspects advisory wording — structured deltas only. Scraper drift
→ exit 3, reported distinctly (fix scorecard.mjs, not a regression).
Portal unreachable → exit 0 (inconclusive, never a false fail). Gates
nothing in the release path; no PR/release triggers; no dependency
install (Node builtins only).

`make scorecard-gate` runs it; `make scorecard-baseline` re-snapshots
deliberately (refuses on drift) — a named act, since an accidental
re-baseline silently disarms the gate. Baseline captured for 0.11.25
(Health Excellent 4/4, Review Satisfactory 3/4, 5 issues, 12 findings)
— still shows Dynamic Code Execution; it clears (an improvement, not a
fail) when a post-ADR-201 release is scanned.

Validated: happy path exit 0, simulated regression exit 1 (all 3
classes), drift refusal, idempotent re-baseline.

Closes #165
2026-05-16 23:42:05 -05:00
Aaron Bockelie
c59314d591 fix(scorecard): re-anchor parser to redesigned Obsidian portal (#183)
The 2026 portal redesign replaced the old DOM/RSC structure with a card
UI: Health/Review render as a coloured grade word + a segmented bar
meter, and findings are RSC JSX tuples. The old parser drifted (SCRAPER
DRIFT, null anchors).

- decodeDoc/toText split: keep tags for the structural bar meter, strip
  for prose fields.
- gradeAndScore(): grade = coloured span after the label; numeric score =
  filled/total bar-meter segments (#183 — the portal's visible trust
  signal), window bounded to the next label so Health can't bleed into
  Review.
- Grade vocab no longer enum-pinned (the hard-coded Excellent|Good|… list
  is exactly what drifted) — capture whatever word renders; drift guard
  catches a null.
- Findings re-extracted from ["$","div|details","<text>",{...}] tuples in
  the scorecard region; SIGNATURE re-anchored to the redesign wording
  (**Title**: + neutral scan/attestation sentences).
- Drift guard: numeric scores are now first-class anchors; findingsDrift
  = non-zero issue count with zero findings. Exit 2 on drift preserved.

Validated live: Health Excellent (4/4), Review Satisfactory (3/4), 5
issues, 12 findings, freshness current, integrity ok. Correctly surfaces
the residual "Dynamic Code Execution" finding on 0.11.25 (clears when a
post-ADR-201 release is scanned — the CLAUDE.md caveat).

Refs #183
2026-05-16 23:29:53 -05:00
Aaron Bockelie
2738a557ac Merge remote-tracking branch 'origin/main' into docs/plugin-description
# Conflicts:
#	Makefile
2026-05-16 12:11:53 -05:00
Aaron Bockelie
46ed67361a chore(meta): make pattern for plugin description; rewrite copy
package.json is now the single source of truth for the plugin description
(as it already effectively is for version). sync-version.mjs propagates it
into manifest.json; mcpb/manifest.json keeps its Claude-Desktop-specific
description and is intentionally not synced.

Adds scripts/set-description.mjs + 'make set-description DESC=...' so neither
JSON file is hand-edited (250-char Obsidian limit enforced).

Rewrites the word-salad description to reality-based copy naming the actual
client and mechanism:
  'Give Claude Desktop and other AI assistants semantic access to your notes
   through a built-in Model Context Protocol (MCP) server.'
2026-05-16 12:07:52 -05:00
Aaron Bockelie
86eb4df569 feat(scorecard): pull Obsidian portal scorecard as free post-release signal
Add scripts/scorecard.mjs + 'make scorecard'. The community portal renders a
deterministic Health/Review scan; the page is server-rendered (summary in DOM,
findings in the Next.js RSC payload) so we can pull it without a browser.

- Computes a freshness delta (portal version vs manifest.json) since a fresh
  scan only triggers from the authenticated dev portal — STALE means a
  logged-in re-scan is still needed.
- Scraper drift guard: missing critical anchors / empty findings on a
  non-clean Review exits non-zero with a SCRAPER DRIFT banner so the parser
  rots loudly, not silently. Scorecard body never gates.
- --json mode for diffing findings across releases.

CLAUDE.md: document make scorecard, the freshness caveat, the drift guard,
and the known accepted cautions (.mcpb additional-files is ADR-102 by design;
'scan not available' disclosures are neutral). Actionable findings tracked in
#163 (rejectUnauthorized), #164 (attestation); CI-gate in #165.
2026-05-16 11:53:47 -05:00
Aaron Bockelie
4d46014b8a fix(build): Use stable latest-release URL for MCPB download
The Settings UI was building a versioned releases/download/<version>/
URL that would 404 for any plugin version not yet associated with a
GitHub release — the exact case for users on a fresh BRAT install
running ahead of the release pipeline.

build-mcpb.mjs now emits both the versioned filename (for archival)
and an unversioned obsidian-mcp.mcpb alias. release.yml uploads both.
The Settings UI links to releases/latest/download/obsidian-mcp.mcpb,
which GitHub resolves to the most recent release's asset by name —
stable regardless of plugin version.

Makefile clean target updated to remove the new artifact.

Refs PR #155 review.
2026-05-15 11:46:06 -05:00
Aaron Bockelie
7f77b72b6c feat: Add scripts/make-mcpb.mjs interactive maker for advanced users
Power users with multiple Obsidian vaults (or anyone wanting a
custom-named bundle) run `node scripts/make-mcpb.mjs`, answer three
prompts (display name, URL, API key), and get an
obsidian-mcp-<slug>.mcpb with the chosen name and defaults
pre-filled. Drop it into Claude Desktop — one-click install with no
manual entry in the user_config dialog.

Shares the zip builder with scripts/build-mcpb.mjs so there's a
single source of truth for the bundle format. Zero deps; Node ≥18.

This is the path ADR-102 §6 designates for multi-vault setups,
replacing both the rejected N-slot manifest design and the
multi-variant publishing alternative.

Refs ADR-102.
2026-05-15 11:31:57 -05:00
Aaron Bockelie
7ca31c4685 build: Add make mcpb target and attach .mcpb to release assets
scripts/build-mcpb.mjs is a zero-dep pure-Node zip builder
(stored-only container, manual CRC32) that packs mcpb/manifest.json
and mcpb/server.js into obsidian-mcp-<version>.mcpb. Works on any
Node ≥18, doesn't require system zip/7z, runs identically on dev
machines and in CI.

Makefile gains a "mcpb" target so the local workflow is "make mcpb"
when iterating. clean now also removes built .mcpb files.

release.yml builds the bundle as part of the release job and attaches
it to the GitHub release alongside main.js/manifest.json/styles.css,
so BRAT users and the future plugin Settings "download" affordance
both resolve to the canonical release asset.

Refs ADR-102.
2026-05-15 11:30:50 -05:00