mirror of
https://github.com/israerusan/patina.git
synced 2026-07-22 08:31:33 +00:00
Excluded folders excluded nothing but the queue and the CSV. The setting promises "never scored or listed", but isExcluded() was consulted only inside buildQueue: DecayIndex.rebuild() scored every markdown file, so the file explorer still dimmed Archive/2019 Retro.md and the status bar still read "Decay 91 · decayed" on it. Skip excluded paths in rebuild() — every surface reads the index, so that is the one place the rule belongs. Excluded notes still count as inbound LINK SOURCES: a link from the Archive is still a link, and dropping it would make everything the Archive cites look more abandoned than it is. The MutationObserver watched document.body with subtree:true. Obsidian rewrites the editor DOM on every keystroke, so it fired continuously while the user typed and ran a document-wide querySelectorAll every 150ms. Observe the file explorer's container instead (DESIGN 8.1), re-resolved on layout-change so an explorer that is opened, closed, or dragged to the other sidebar is still followed. A non-string licenseKey in data.json crashed onload(): TypeError: this.settings.licenseKey.trim is not a function thrown from refreshLicense(), two lines into onload() — so the plugin never loaded, its settings tab never rendered, and the bad value could not be fixed from inside Obsidian. loadSettings() coerced isPro, excludeFolders, snoozedUntil, halfLives, weights and bandThresholds, and not one string. Coerce the strings and the arithmetic numbers (snoozeDays: "x" made Date.now() + NaN, a snooze that silently never happened). styles.css now carries the block EngineInstallModal's header requires every vendoring plugin to paste. The modal is unreachable in 1.0.0 and tree-shakes out, but whoever wires the download must not inherit an unstyled consent dialog — that is the one screen that asks the user to let this add-on run a binary. A test parses the class names out of the modal source and asserts each has a rule. community-plugins.json staged "author": "saiken" against a manifest that says Israel Avila. That file is the PR body for obsidian-releases, submitted verbatim. Engine binaries are now re-hashed before every spawn — see the shared core commit 43ba784. InstalledInfo.sha256 was the digest of an archive that install() deletes, so it could never guard anything; the digest of the executable itself is recorded and checked in resolveExecutable(), and a swapped binary is refused. Regression tests, each proved to fail against the unfixed source: decay-index "an excluded note is not in the index" explorer-decorator "the observer must watch the file explorer's container" settings-coercion "settings.licenseKey must be a string after load" manifest-contract "the staged author must be the manifest author" Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R3f6GZ2A8FbZerhJEpJQBE
240 lines
10 KiB
JavaScript
240 lines
10 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Sync the vendored shared core in a plugin repo against the canonical
|
|
* obsidian-plugin-core checkout.
|
|
*
|
|
* Run from a plugin repo root (each plugin vendors a copy of this script):
|
|
* node scripts/sync-shared.mjs # copy canonical -> src/shared/
|
|
* node scripts/sync-shared.mjs --check # exit 1 if the vendored copy drifted
|
|
*
|
|
* Run from obsidian-plugin-core itself:
|
|
* node scripts/sync-shared.mjs --manifest # regenerate shared/MANIFEST.sha256
|
|
*
|
|
* THE DRIFT GATE HAS TO WORK WITHOUT THE CANONICAL REPO. This script used to exit 0 from
|
|
* `--check` whenever ../obsidian-plugin-core was absent — which is ALWAYS true in CI and in a
|
|
* fresh clone. So `npm test`'s drift check enforced nothing on any pull request, and a
|
|
* hand-edited `src/shared/verifyLicense.mjs` merged green: exactly the drift class this repo
|
|
* exists to prevent, and exactly how two divergent copies of the verifier happened.
|
|
*
|
|
* So `sync:shared` also writes MANIFEST.sha256 — the SHA-256 of every synced file — INTO the
|
|
* vendored tree, and `--check` validates the vendored files against that manifest when the
|
|
* canonical repo is not around. Editing a vendored file now fails the check on any machine,
|
|
* with or without the canonical checkout; editing the manifest to match is a deliberate act
|
|
* that shows up in the diff of a file whose header says not to.
|
|
*/
|
|
import crypto from "node:crypto";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { pathToFileURL } from "node:url";
|
|
|
|
/** [canonical-relative, plugin-relative] pairs kept in sync. */
|
|
const FILES = [
|
|
["shared/verifyLicense.mjs", "src/shared/verifyLicense.mjs"],
|
|
["shared/verifyLicense.d.mts", "src/shared/verifyLicense.d.mts"],
|
|
["shared/suiteLicense.mjs", "src/shared/suiteLicense.mjs"],
|
|
["shared/suiteLicense.d.mts", "src/shared/suiteLicense.d.mts"],
|
|
["shared/revokedLicenses.mjs", "src/shared/revokedLicenses.mjs"],
|
|
["shared/revokedLicenses.d.mts", "src/shared/revokedLicenses.d.mts"],
|
|
["shared/licenseTransition.mjs", "src/shared/licenseTransition.mjs"],
|
|
["shared/licenseTransition.d.mts", "src/shared/licenseTransition.d.mts"],
|
|
["shared/featureGates.mjs", "src/shared/featureGates.mjs"],
|
|
["shared/featureGates.d.mts", "src/shared/featureGates.d.mts"],
|
|
["shared/hash.mjs", "src/shared/hash.mjs"],
|
|
["shared/hash.d.mts", "src/shared/hash.d.mts"],
|
|
["shared/engine/protocol.mjs", "src/shared/engine/protocol.mjs"],
|
|
["shared/engine/protocol.d.mts", "src/shared/engine/protocol.d.mts"],
|
|
["shared/engine/chunk.mjs", "src/shared/engine/chunk.mjs"],
|
|
["shared/engine/chunk.d.mts", "src/shared/engine/chunk.d.mts"],
|
|
["shared/engine/installPlan.mjs", "src/shared/engine/installPlan.mjs"],
|
|
["shared/engine/installPlan.d.mts", "src/shared/engine/installPlan.d.mts"],
|
|
["shared/engine/engineRelease.mjs", "src/shared/engine/engineRelease.mjs"],
|
|
["shared/engine/engineRelease.d.mts", "src/shared/engine/engineRelease.d.mts"],
|
|
["shared/engine/EngineHost.ts", "src/shared/engine/EngineHost.ts"],
|
|
["shared/engine/EngineBroker.ts", "src/shared/engine/EngineBroker.ts"],
|
|
["shared/engine/EngineInstallModal.ts", "src/shared/engine/EngineInstallModal.ts"],
|
|
["shared/signals/signalsAggregate.mjs", "src/shared/signals/signalsAggregate.mjs"],
|
|
["shared/signals/signalsAggregate.d.mts", "src/shared/signals/signalsAggregate.d.mts"],
|
|
["shared/signals/SignalStore.ts", "src/shared/signals/SignalStore.ts"],
|
|
["shared/signals/SignalsBroker.ts", "src/shared/signals/SignalsBroker.ts"],
|
|
["scripts/sync-shared.mjs", "scripts/sync-shared.mjs"],
|
|
];
|
|
|
|
/** Generated, not synced: it is the checksum OF the synced files. */
|
|
const MANIFEST = ["shared/MANIFEST.sha256", "src/shared/MANIFEST.sha256"];
|
|
|
|
const MANIFEST_HEADER = [
|
|
"# SHA-256 of every file in the vendored shared tree, keyed by its canonical path.",
|
|
"# GENERATED by scripts/sync-shared.mjs. Do not edit, and do not edit a shared file",
|
|
"# without re-running `npm run sync:shared` — `npm test` verifies this manifest even",
|
|
"# when the canonical obsidian-plugin-core checkout is not present (i.e. in CI).",
|
|
];
|
|
|
|
/** Compare content, not line endings — git autocrlf checkouts differ per OS. */
|
|
const normalize = (text) => text.replace(/\r\n/g, "\n");
|
|
|
|
const sha256 = (text) => crypto.createHash("sha256").update(normalize(text), "utf8").digest("hex");
|
|
|
|
/**
|
|
* @param {string} root repo root
|
|
* @param {(pair: string[]) => string} pick canonical-relative or plugin-relative
|
|
* @returns {string} the manifest body
|
|
*/
|
|
export function buildManifest(root, pick) {
|
|
const lines = [...MANIFEST_HEADER];
|
|
for (const pair of FILES) {
|
|
const file = path.join(root, pick(pair));
|
|
if (!fs.existsSync(file)) throw new Error(`sync-shared: cannot hash a missing file: ${file}`);
|
|
lines.push(`${sha256(fs.readFileSync(file, "utf8"))} ${pair[0]}`);
|
|
}
|
|
return lines.join("\n") + "\n";
|
|
}
|
|
|
|
const canonicalManifest = (root) => buildManifest(root, ([from]) => from);
|
|
const vendoredManifest = (root) => buildManifest(root, ([, to]) => to);
|
|
|
|
/** Write `body` to `file` only when it would change it. Returns true when it wrote. */
|
|
function writeIfChanged(file, body) {
|
|
const have = fs.existsSync(file) ? fs.readFileSync(file, "utf8") : null;
|
|
if (have !== null && normalize(have) === normalize(body)) return false;
|
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
fs.writeFileSync(file, body);
|
|
return true;
|
|
}
|
|
|
|
/** The vendored tree, checked against the manifest that shipped with it. No canonical repo
|
|
* needed — this is the check that actually runs on a pull request. */
|
|
function checkAgainstManifest(repoRoot) {
|
|
const manifestPath = path.join(repoRoot, MANIFEST[1]);
|
|
if (!fs.existsSync(manifestPath)) {
|
|
console.error(
|
|
`sync-shared: ${MANIFEST[1]} is missing — the vendored tree cannot be verified.\n` +
|
|
'Run "npm run sync:shared" against the canonical obsidian-plugin-core checkout.'
|
|
);
|
|
return 1;
|
|
}
|
|
|
|
let want;
|
|
try {
|
|
want = vendoredManifest(repoRoot);
|
|
} catch (error) {
|
|
console.error(String(error instanceof Error ? error.message : error));
|
|
return 1;
|
|
}
|
|
|
|
const have = fs.readFileSync(manifestPath, "utf8");
|
|
if (normalize(have) === normalize(want)) {
|
|
console.log(`sync-shared: vendored tree matches ${MANIFEST[1]} (${FILES.length} files).`);
|
|
return 0;
|
|
}
|
|
|
|
// Name the files, not just the mismatch — the whole point is to say what was hand-edited.
|
|
const expected = new Map(
|
|
normalize(have)
|
|
.split("\n")
|
|
.filter((line) => line && !line.startsWith("#"))
|
|
.map((line) => {
|
|
const [hash, file] = line.split(/\s+/);
|
|
return [file, hash];
|
|
})
|
|
);
|
|
let drifted = 0;
|
|
for (const [from, to] of FILES) {
|
|
const file = path.join(repoRoot, to);
|
|
const actual = fs.existsSync(file) ? sha256(fs.readFileSync(file, "utf8")) : "<missing>";
|
|
if (expected.get(from) !== actual) {
|
|
console.error(`sync-shared: DRIFT in ${to} (does not match ${MANIFEST[1]})`);
|
|
drifted++;
|
|
}
|
|
}
|
|
if (drifted === 0) {
|
|
console.error(`sync-shared: ${MANIFEST[1]} does not match the files it lists — it was edited by hand.`);
|
|
drifted = 1;
|
|
}
|
|
console.error(
|
|
`sync-shared: ${drifted} file(s) drifted from the canonical shared tree. Edit them in ` +
|
|
'obsidian-plugin-core and run "npm run sync:shared" — never in this repo.'
|
|
);
|
|
return 1;
|
|
}
|
|
|
|
function main(argv) {
|
|
const repoRoot = process.cwd();
|
|
const canonical = process.env.PLUGIN_CORE_PATH || path.resolve(repoRoot, "..", "obsidian-plugin-core");
|
|
const check = argv.includes("--check");
|
|
const manifestOnly = argv.includes("--manifest");
|
|
|
|
// Regenerate the canonical manifest in obsidian-plugin-core itself. Its own test suite
|
|
// runs this in --check mode, so a shared file edited without regenerating fails there too.
|
|
if (manifestOnly) {
|
|
const file = path.join(repoRoot, MANIFEST[0]);
|
|
const body = canonicalManifest(repoRoot);
|
|
if (check) {
|
|
const have = fs.existsSync(file) ? fs.readFileSync(file, "utf8") : "";
|
|
if (normalize(have) !== normalize(body)) {
|
|
console.error(`sync-shared: ${MANIFEST[0]} is stale — run "npm run manifest:shared".`);
|
|
return 1;
|
|
}
|
|
console.log(`sync-shared: ${MANIFEST[0]} is current.`);
|
|
return 0;
|
|
}
|
|
console.log(writeIfChanged(file, body) ? `sync-shared: wrote ${MANIFEST[0]}.` : `sync-shared: ${MANIFEST[0]} is current.`);
|
|
return 0;
|
|
}
|
|
|
|
if (!fs.existsSync(canonical)) {
|
|
// No canonical checkout: CI, or a fresh clone. This is the case the old script treated
|
|
// as "nothing to do", which is what made the gate decorative. The vendored manifest is
|
|
// the authority here.
|
|
if (check) return checkAgainstManifest(repoRoot);
|
|
console.log(`sync-shared: canonical repo not found at ${canonical} — nothing to copy.`);
|
|
return 0;
|
|
}
|
|
|
|
let drifted = 0;
|
|
for (const [from, to] of FILES) {
|
|
const src = path.join(canonical, from);
|
|
const dest = path.join(repoRoot, to);
|
|
if (!fs.existsSync(src)) {
|
|
console.error(`sync-shared: missing canonical file ${src}`);
|
|
return 1;
|
|
}
|
|
const want = fs.readFileSync(src, "utf8");
|
|
const have = fs.existsSync(dest) ? fs.readFileSync(dest, "utf8") : null;
|
|
if (have !== null && normalize(want) === normalize(have)) continue;
|
|
if (check) {
|
|
console.error(`sync-shared: DRIFT in ${to} (canonical: ${from})`);
|
|
drifted++;
|
|
} else {
|
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
fs.writeFileSync(dest, want);
|
|
console.log(`sync-shared: updated ${to}`);
|
|
}
|
|
}
|
|
|
|
if (check) {
|
|
if (drifted > 0) {
|
|
console.error(`sync-shared: ${drifted} file(s) drifted — run "npm run sync:shared" to update.`);
|
|
return 1;
|
|
}
|
|
// The files match the canonical tree; the manifest must match them too, or CI (which
|
|
// has no canonical tree and can only consult the manifest) would disagree with us.
|
|
return checkAgainstManifest(repoRoot);
|
|
}
|
|
|
|
// Keep both manifests honest: the canonical one, so obsidian-plugin-core's own test passes,
|
|
// and the vendored one, which is the only thing CI can check.
|
|
const body = canonicalManifest(canonical);
|
|
if (writeIfChanged(path.join(canonical, MANIFEST[0]), body)) {
|
|
console.log(`sync-shared: refreshed ${MANIFEST[0]} in the canonical repo`);
|
|
}
|
|
if (writeIfChanged(path.join(repoRoot, MANIFEST[1]), body)) console.log(`sync-shared: updated ${MANIFEST[1]}`);
|
|
console.log("sync-shared: done.");
|
|
return 0;
|
|
}
|
|
|
|
// Importable (obsidian-plugin-core's own manifest test uses buildManifest) and runnable.
|
|
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
process.exit(main(process.argv.slice(2)));
|
|
}
|
|
|
|
export { FILES, MANIFEST, main };
|