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
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
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
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.'
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.
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.
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.
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.