Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 43 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,34 @@ All notable changes to this project are documented here.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

Three §5 trust signals that didn't do what they promised. The derived trust tier
and `sources[].author` were described as shipped in the 0.3.0 notes and the
README but were never surfaced or checked; `stale_after` was validated for its
form only, so a review deadline could pass unnoticed.

### Added
- **`stale_after` is enforced, not just parsed** ([#10](https://github.com/MartinForReal/okf-enforcer/issues/10)).
A note whose `stale_after` has passed is flagged as due for review; previously
only the date's *form* was checked, so a deadline from last year passed
silently. Advisory only — §11 keeps it out of conformance.
- **The derived trust tier is shown** ([#11](https://github.com/MartinForReal/okf-enforcer/issues/11)).
The active note's tier (unverified / machine-confirmed / human-reviewed, §5.3)
appears in the status-bar tooltip. It was computed but reached no surface. The
report pane lists only files that *have* issues, so a conformant note would
never show a tier there.
- **`sources[].author` is validated** ([#12](https://github.com/MartinForReal/okf-enforcer/issues/12)).
Warns when an entry's `author` is present but not a non-empty string,
alongside the existing `resource` / `usage_count` / `last_modified` checks.

All three sit behind **Validate trust & lifecycle fields**, off by default, so no
vault starts reporting anything new unless it had already opted in.

### Fixed
- README and the 0.3.0 changelog entry claimed derived trust tiers and
`sources[].author` validation that had not shipped. Corrected.

## [0.4.1] - 2026-07-31

### Changed
Expand Down Expand Up @@ -254,10 +282,16 @@ the fallbacks below, so existing vaults keep validating.
### Added
- **Provenance / trust / lifecycle validation** (§5), opt-in via **Validate trust
& lifecycle fields**. When present, checks `generated`/`verified` shape and the
actor convention (`<producer>/<version>`, `human:<id>`, `process:<id>`), derives
trust tiers (unverified / machine-confirmed / human-reviewed), and validates
`status` (`draft|stable|deprecated`), `stale_after` (absolute date), and
`sources` with its credibility signals (`author`/`usage_count`/`last_modified`).
actor convention (`<producer>/<version>`, `human:<id>`, `process:<id>`), and
validates `status` (`draft|stable|deprecated`), `stale_after` (absolute date),
and `sources` (`resource`, `usage_count`, `last_modified`).

> **Correction.** This entry originally read "derives trust tiers (unverified /
> machine-confirmed / human-reviewed)" and listed `author` among the validated
> `sources` signals. Neither was true of 0.3.0: the tier was computed but never
> shown anywhere, and `author` was not checked. Both are fixed in Unreleased
> ([#11](https://github.com/MartinForReal/okf-enforcer/issues/11),
> [#12](https://github.com/MartinForReal/okf-enforcer/issues/12)).
- **Attested Computation concepts** (§10). A `type: Attested Computation` note is
checked for a required `runtime`, a present computation (inline `# Computation`
fence or a `computation` path), and `parameters`/`executor`/`attester` shape.
Expand Down Expand Up @@ -408,8 +442,13 @@ Initial release.
- Batched, non-blocking scan/fix queue with an inline progress bar for large vaults.
- Settings for automation toggles, batch size, warning rules, and excluded folders.

[Unreleased]: https://github.com/MartinForReal/okf-enforcer/compare/0.4.1...HEAD
[0.4.1]: https://github.com/MartinForReal/okf-enforcer/releases/tag/0.4.1
[0.4.0]: https://github.com/MartinForReal/okf-enforcer/releases/tag/0.4.0
[0.3.0]: https://github.com/MartinForReal/okf-enforcer/releases/tag/0.3.0
[0.2.2]: https://github.com/MartinForReal/okf-enforcer/releases/tag/0.2.2
[0.2.1]: https://github.com/MartinForReal/okf-enforcer/releases/tag/0.2.1
[0.2.0]: https://github.com/MartinForReal/okf-enforcer/releases/tag/0.2.0
[0.1.3]: https://github.com/MartinForReal/okf-enforcer/releases/tag/0.1.3
[0.1.2]: https://github.com/MartinForReal/okf-enforcer/releases/tag/0.1.2
[0.1.1]: https://github.com/MartinForReal/okf-enforcer/releases/tag/0.1.1
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ OKF is an open, minimal convention for representing knowledge as a directory of
## Features

- **Conformance validation.** Checks every note against OKF v0.2. The hard rules (parseable frontmatter, non-empty `type`, valid `index.md`/`log.md` structure — §11/§8/§9) are reported as **errors**; the spec's recommended fields and SHOULD-guidance are **warnings** you can toggle. The spec's permissive rules are respected — broken links and missing optional fields never fail a bundle.
- **Trust & provenance (v0.2).** Opt-in checks for the §5 families: `generated`/`verified` and the actor convention, derived trust tiers (unverified / machine-confirmed / human-reviewed), `status` (`draft|stable|deprecated`), `stale_after`, and `sources` with its credibility signals.
- **Trust & provenance (v0.2).** Opt-in checks for the §5 families: `generated`/`verified` and the actor convention, `status` (`draft|stable|deprecated`), `stale_after` — both that it's an absolute date and whether it has passed, so a note that's due for review says so — and `sources` with its credibility signals (`author`/`usage_count`/`last_modified`). The note's derived trust tier (unverified / machine-confirmed / human-reviewed) is shown in the status-bar tooltip.
- **Attested Computation (v0.2).** Validates `type: Attested Computation` concepts (§10) — a required `runtime`, a present computation (inline `# Computation` fence or a `computation` path), and `parameters`/`executor`/`attester` shape.
- **v0.1 → v0.2 migration.** A command rewrites a legacy `timestamp` into `generated: { by, at }` and lifts a body `# Citations` list into `sources`, then offers to declare `okf_version: "0.2"` in the root index.
- **Vault-wide report.** A compact, collapsible side panel lists every non-conformant note, errors first, with a one-line summary of conformant / error / warning counts. Hidden by default; opens only on demand.
- **Status-bar indicator.** A single status-bar item shows the active note's state (`✓` / `⚠` / `✖`) with details in its tooltip. Click it to auto-fix the current note.
- **Status-bar indicator.** A single status-bar item shows the active note's state (`✓` / `⚠` / `✖`) with details in its tooltip — including the note's derived trust tier, when trust checks are on. Click it to auto-fix the current note.
- **Auto-fix.** Inserts missing frontmatter (`type`, `title`, `generated`) non-destructively — it never overwrites values you've set.
- **Prompt for required fields.** When a note is missing a meaningful `type`, a dialog lets you set `type`, `title`, and `description` directly.
- **On-save & on-create hooks.** New notes, edited notes, and notes added by the **Importer** plugin are brought into conformance automatically.
Expand Down Expand Up @@ -53,7 +53,7 @@ Configure under **Settings → OKF Enforcer**:
- **Generate index.md on startup** — off by default, since the hooks above already keep listings current while the plugin is running. Turn it on to bring every folder's `index.md` up to date once when the plugin loads, for what changed while Obsidian was closed — a vault synced from another machine, or edited outside it. It runs before the startup scan, so the scan judges the listings as they now stand rather than the ones it is about to replace, and it runs quietly: progress shows in the status bar, with no notice at the end.
- **Rebuild existing index.md** — off by default, so generating over an existing index adds what's missing, fixes links that point at the wrong path, and removes entries for notes that are gone, changing nothing else. Turn it on to rewrite the listing from the folder's contents instead, which also refreshes every description, re-sorts the entries, and re-groups them under their current `type` — at the cost of any prose in the file, which a rebuild discards apart from the section named below.
- **Subdirectory description section** — heading in a subfolder's `index.md` whose first paragraph becomes that folder's description in the parent listing (e.g. `Purpose`). Blank by default, which leaves subdirectory entries undescribed. Additive generation never touches the file's prose, and this is the one section a rebuild carries over, so the description you write survives a refresh either way.
- **Rules** — **Warn on missing recommended fields**, **Warn on missing tags**, **Validate trust & lifecycle fields**, and **Validate Attested Computation concepts** select which optional checks to surface. `index.md` / `log.md` structure is always checked: §11 makes it one of the three requirements for a conformant bundle, so it isn't a toggle.
- **Rules** — **Warn on missing recommended fields**, **Warn on missing tags**, **Validate trust & lifecycle fields**, and **Validate Attested Computation concepts** select which optional checks to surface. **Validate trust & lifecycle fields** also decides whether the note's trust tier appears in the status-bar tooltip, since the tier is derived from the same §5 `verified` events the checks read. `index.md` / `log.md` structure is always checked: §11 makes it one of the three requirements for a conformant bundle, so it isn't a toggle.
- **Excluded folders** — paths skipped during validation and index generation (default: `Templates`). Use this for attachment folders you'd rather not have an `index.md` in. The Obsidian config folder (e.g. `.obsidian`) is always skipped automatically.
- **Batch size** — files processed per async chunk (lower = smoother UI on very large vaults).
- **Enable Portent validation** _(experimental / beta — the Portent spec is pre-1.0 and may change)_ — layer the [Portent](https://portent.md) spec on top of OKF: default type vocabulary (`Project`, `Operation`, `Responsibility`, `Task`, `Event`, `Note`, `Topic`, `Person`), lifecycle metadata (optional and format-free — a single `status`/`state` value, boolean `organized`/`archived`, or omitted entirely when organized by default), and relationship shape (`belongs_to` single wikilink, `related_to` list of wikilinks). All Portent findings are warnings — they never break OKF conformance.
Expand Down
67 changes: 58 additions & 9 deletions main.js
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,36 @@ function normalizeVerified(data) {
if (typeof v === "object") return [v];
return [];
}
function trustTier(data) {
var _a;
const events = normalizeVerified(data);
if (events.length === 0) return "unverified";
for (const e of events) {
if (String((_a = e["by"]) != null ? _a : "").startsWith("human:")) return "human-reviewed";
}
return "machine-confirmed";
}
function trustTierOfContent(content) {
const { hasFm, raw } = splitFrontmatter(content);
if (!hasFm) return null;
try {
const parsed = (0, import_obsidian.parseYaml)(raw);
if (parsed && typeof parsed === "object") {
return trustTier(parsed);
}
} catch (e) {
return null;
}
return null;
}
function isStale(data, today = /* @__PURE__ */ new Date()) {
const raw = data["stale_after"];
if (!raw) return false;
const s = String(raw).slice(0, 10);
if (!ISO_DATE_RE.test(s)) return false;
const t = today.toISOString().slice(0, 10);
return t >= s;
}
function basename(path) {
const f = path.split("/").pop() || path;
return f.replace(/\.md$/i, "");
Expand Down Expand Up @@ -779,6 +809,12 @@ function validateTrustFamilies(data) {
rule: "\xA75.5",
message: "`stale_after` should be an absolute date (`YYYY-MM-DD`)."
});
} else if (isStale(data)) {
issues.push({
severity: "warning",
rule: "\xA75.5",
message: `\`stale_after\` (${s}) has passed; this concept is due for review.`
});
}
}
if ("sources" in data && data["sources"] != null) {
Expand Down Expand Up @@ -807,6 +843,13 @@ function validateTrustFamilies(data) {
message: `\`sources[${i}]\` is missing the required \`resource\`.`
});
}
if ("author" in e && (typeof e["author"] !== "string" || e["author"].trim().length === 0)) {
issues.push({
severity: "warning",
rule: "\xA75.1",
message: `\`sources[${i}].author\` should be a non-empty string.`
});
}
if ("usage_count" in e && typeof e["usage_count"] !== "number") {
issues.push({
severity: "warning",
Expand Down Expand Up @@ -1577,7 +1620,7 @@ var OkfPlugin = class extends import_obsidian3.Plugin {
this.isRoot(file),
this.settings
);
this.updateStatus(postIssues);
this.updateStatus(postIssues, content);
const remainingErrors = postIssues.filter((i) => i.severity === "error");
if (remainingErrors.length > 0) {
this.promptForRequiredFields(file, remainingErrors);
Expand All @@ -1599,16 +1642,17 @@ var OkfPlugin = class extends import_obsidian3.Plugin {
this.isRoot(file),
this.settings
);
this.updateStatus(issues);
this.updateStatus(issues, content);
if (openReport) {
this.renderResults(issues.length ? [{ path: file.path, issues }] : [], 1);
void this.activateView();
if (!issues.length) new import_obsidian3.Notice("OKF: active note is conformant \u2705");
}
}
updateStatus(issues) {
updateStatus(issues, content) {
const errs = issues.filter((i) => i.severity === "error").length;
const warns = issues.filter((i) => i.severity === "warning").length;
const tier = this.settings.warnTrustFields && content !== void 0 ? trustTierOfContent(content) : null;
this.statusEl.removeClass(
"okf-statusbar-ok",
"okf-statusbar-bad",
Expand All @@ -1625,13 +1669,18 @@ var OkfPlugin = class extends import_obsidian3.Plugin {
this.statusEl.addClass("okf-statusbar-ok");
}
if (issues.length === 0) {
this.statusEl.setAttribute(
"aria-label",
"Active note conforms to OKF v0.2 \u2014 click to scan the vault"
);
const lines = ["Active note conforms to OKF v0.2"];
if (tier) lines.push(`Trust tier: ${tier}`);
lines.push("");
lines.push("Click to scan the whole vault");
this.statusEl.setAttribute("aria-label", lines.join("\n"));
} else {
const lines = issues.slice(0, 8).map((i) => `${i.severity === "error" ? "\u2716" : "\u26A0"} ${i.rule} ${i.message}`);
if (issues.length > 8) lines.push(`\u2026and ${issues.length - 8} more`);
if (tier) {
lines.push("");
lines.push(`Trust tier: ${tier}`);
}
lines.push("");
lines.push("Click to scan the whole vault");
this.statusEl.setAttribute("aria-label", lines.join("\n"));
Expand Down Expand Up @@ -1781,7 +1830,7 @@ Click to open the report`
this.isRoot(file),
this.settings
);
this.updateStatus(issues);
this.updateStatus(issues, content);
}
async fixAll() {
if (this.busy) {
Expand Down Expand Up @@ -2193,7 +2242,7 @@ var OkfSettingTab = class extends import_obsidian3.PluginSettingTab {
},
{
name: "Validate trust & lifecycle fields",
desc: "Check the v0.2 trust fields on notes that carry them: `verified`, `status`, `stale_after`, `sources` (\xA75). Advisory; off by default.",
desc: "Check the v0.2 trust fields on notes that carry them: `verified`, `status`, `stale_after` (including whether it has passed), `sources` (\xA75), and show the note's trust tier in the status-bar tooltip. Advisory; off by default.",
control: (row) => row.addToggle(
(tg) => tg.setValue(s.warnTrustFields).onChange((v) => {
s.warnTrustFields = v;
Expand Down
31 changes: 22 additions & 9 deletions main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
DEFAULT_SETTINGS,
validateContent,
applyFixes,
trustTierOfContent,
isReserved,
isExcluded,
basename,
Expand Down Expand Up @@ -409,7 +410,7 @@ export default class OkfPlugin extends Plugin {
this.isRoot(file),
this.settings
);
this.updateStatus(postIssues);
this.updateStatus(postIssues, content);

const remainingErrors = postIssues.filter((i) => i.severity === "error");
if (remainingErrors.length > 0) {
Expand Down Expand Up @@ -437,17 +438,24 @@ export default class OkfPlugin extends Plugin {
this.isRoot(file),
this.settings
);
this.updateStatus(issues);
this.updateStatus(issues, content);
if (openReport) {
this.renderResults(issues.length ? [{ path: file.path, issues }] : [], 1);
void this.activateView();
if (!issues.length) new Notice("OKF: active note is conformant ✅");
}
}

private updateStatus(issues: OkfIssue[]) {
private updateStatus(issues: OkfIssue[], content?: string) {
const errs = issues.filter((i) => i.severity === "error").length;
const warns = issues.filter((i) => i.severity === "warning").length;
// The derived trust tier (§5.3) describes the note rather than faulting it,
// so it rides in the tooltip instead of the issue list. The report pane only
// lists files that have issues, which would hide the tier on a clean note.
const tier =
this.settings.warnTrustFields && content !== undefined
? trustTierOfContent(content)
: null;
this.statusEl.removeClass(
"okf-statusbar-ok",
"okf-statusbar-bad",
Expand All @@ -465,15 +473,20 @@ export default class OkfPlugin extends Plugin {
}
// Tooltip carries the detail so we don't need a Notice for routine checks.
if (issues.length === 0) {
this.statusEl.setAttribute(
"aria-label",
"Active note conforms to OKF v0.2 — click to scan the vault"
);
const lines = ["Active note conforms to OKF v0.2"];
if (tier) lines.push(`Trust tier: ${tier}`);
lines.push("");
lines.push("Click to scan the whole vault");
this.statusEl.setAttribute("aria-label", lines.join("\n"));
} else {
const lines = issues
.slice(0, 8)
.map((i) => `${i.severity === "error" ? "✖" : "⚠"} ${i.rule} ${i.message}`);
if (issues.length > 8) lines.push(`…and ${issues.length - 8} more`);
if (tier) {
lines.push("");
lines.push(`Trust tier: ${tier}`);
}
lines.push("");
lines.push("Click to scan the whole vault");
this.statusEl.setAttribute("aria-label", lines.join("\n"));
Expand Down Expand Up @@ -627,7 +640,7 @@ export default class OkfPlugin extends Plugin {
this.isRoot(file),
this.settings
);
this.updateStatus(issues);
this.updateStatus(issues, content);
}

async fixAll() {
Expand Down Expand Up @@ -1158,7 +1171,7 @@ class OkfSettingTab extends PluginSettingTab {
},
{
name: "Validate trust & lifecycle fields",
desc: "Check the v0.2 trust fields on notes that carry them: `verified`, `status`, `stale_after`, `sources` (§5). Advisory; off by default.",
desc: "Check the v0.2 trust fields on notes that carry them: `verified`, `status`, `stale_after` (including whether it has passed), `sources` (§5), and show the note's trust tier in the status-bar tooltip. Advisory; off by default.",
control: (row) =>
row.addToggle((tg) =>
tg.setValue(s.warnTrustFields).onChange((v) => {
Expand Down
Loading
Loading