diff --git a/Wiki/changelog/cli.md b/Wiki/changelog/cli.md index beb0ead..a082387 100644 --- a/Wiki/changelog/cli.md +++ b/Wiki/changelog/cli.md @@ -13,6 +13,25 @@ page records release-level changes. ## Unreleased +### 2026-08-03 — Separate link-escape rule from link-target + +- `okn validate` now reports a link that resolves outside the bundle root + under a separate `link-escape` rule instead of `link-target`. The + unconfigured default is unchanged: the warning still appears by default. + The split lets a bundle silence escape warnings alone via + `link-escape = "off"` in `.openknowledge.toml` (or `--rule link-escape=off`) + without losing `link-target` detection for in-bundle links that do not + exist. Integrated knowledge bases that link to surrounding repository source + files can keep missing-link detection while suppressing the escape noise. +- **Migration:** an existing `link-target = "off"|"error"` config now affects + only missing-target; add a matching `link-escape` entry to preserve the + prior behavior. JSON consumers keying on `rule: "link-target"` for escape + issues must update to `link-escape`. +- Source: `packages/cli/internal/okf/validation_rules.go`, + `packages/cli/internal/okf/validation_policy.go`, + `packages/cli/internal/okf/validation_checks.go`. +- Docs: `Wiki/features/commands/validate.md`. + ### 2026-08-02 — Unified interactive setup - `okn setup` now starts a terminal wizard. Without terminal input, it prints diff --git a/Wiki/features/commands/validate.md b/Wiki/features/commands/validate.md index 98b53f8..972b460 100644 --- a/Wiki/features/commands/validate.md +++ b/Wiki/features/commands/validate.md @@ -48,7 +48,8 @@ okn validate --quiet Wiki | `frontmatter-format` | warning | Parseable frontmatter follows clean formatting. | | `markdown-syntax` | warning | Links, code spans, tables, and fences look complete. | | `okf-version` | warning | Root `okf_version` matches the selected spec. | -| `link-target` | warning | Local Markdown links resolve inside the bundle. | +| `link-target` | warning | Local Markdown link targets inside the bundle exist. | +| `link-escape` | warning | Local Markdown links resolve inside the bundle root. Disable to allow links to repository files outside the bundle. | The scan includes `.md` and `.markdown` files. It skips `.git`. It classifies `index.md` and `log.md` as reserved files. diff --git a/packages/cli/internal/okf/validate_test.go b/packages/cli/internal/okf/validate_test.go index bbd0889..9cb6eb8 100644 --- a/packages/cli/internal/okf/validate_test.go +++ b/packages/cli/internal/okf/validate_test.go @@ -238,6 +238,56 @@ func TestValidateWarnsForBrokenLocalLinks(t *testing.T) { } } +func TestValidateSeparatesEscapeAndMissingLinkRules(t *testing.T) { + base := t.TempDir() + root := filepath.Join(base, "Wiki") + writeFile(t, base, "src/app.kt", "package demo\n") + writeFile(t, root, "index.md", "# Index\n\n[Code](../src/app.kt)\n[Missing](missing.md)\n") + writeFile(t, root, "log.md", "# Log\n\n## 2026-06-16\n\n* Created.\n") + + find := func(warnings []Issue, rule, substr string) bool { + for _, w := range warnings { + if w.Rule == rule && strings.Contains(w.Message, substr) { + return true + } + } + return false + } + + result, err := Validate(root) + if err != nil { + t.Fatal(err) + } + if len(result.Errors) != 0 { + t.Fatalf("expected no errors, got %#v", result.Errors) + } + if len(result.Warnings) != 2 { + t.Fatalf("expected escape and missing warnings, got %#v", result.Warnings) + } + if !find(result.Warnings, "link-escape", "escapes bundle root") { + t.Fatalf("expected link-escape warning, got %#v", result.Warnings) + } + if !find(result.Warnings, "link-target", "missing.md") { + t.Fatalf("expected link-target missing warning, got %#v", result.Warnings) + } + + result, err = ValidateWithVersionAndOptions(root, LatestSpecVersion, ValidationOptions{Rules: map[string]string{"link-escape": "off"}}) + if err != nil { + t.Fatal(err) + } + if len(result.Warnings) != 1 || !find(result.Warnings, "link-target", "missing.md") { + t.Fatalf("expected only the missing-link warning after disabling link-escape, got %#v", result.Warnings) + } + + result, err = ValidateWithVersionAndOptions(root, LatestSpecVersion, ValidationOptions{Rules: map[string]string{"link-target": "off"}}) + if err != nil { + t.Fatal(err) + } + if len(result.Warnings) != 1 || !find(result.Warnings, "link-escape", "escapes bundle root") { + t.Fatalf("expected only the escape warning after disabling link-target, got %#v", result.Warnings) + } +} + func TestValidateOptionsEscalateAndDisableRules(t *testing.T) { root := t.TempDir() writeFile(t, root, "index.md", "# Index\n\n[Missing](missing.md)\n") diff --git a/packages/cli/internal/okf/validation_checks.go b/packages/cli/internal/okf/validation_checks.go index 5cbfffa..228167f 100644 --- a/packages/cli/internal/okf/validation_checks.go +++ b/packages/cli/internal/okf/validation_checks.go @@ -47,7 +47,7 @@ func buildChecks(result Result) []Check { }, { Name: "Link targets", - Status: statusForErrorWarningRules(result.Errors, result.Warnings, []string{"link-target"}, []string{"link-target"}), + Status: statusForErrorWarningRules(result.Errors, result.Warnings, []string{"link-target", "link-escape"}, []string{"link-target", "link-escape"}), Message: "Local Markdown links should resolve inside the bundle", }, { diff --git a/packages/cli/internal/okf/validation_policy.go b/packages/cli/internal/okf/validation_policy.go index d1c8573..6857caf 100644 --- a/packages/cli/internal/okf/validation_policy.go +++ b/packages/cli/internal/okf/validation_policy.go @@ -30,6 +30,7 @@ var knownValidationRules = map[string]struct{}{ "frontmatter": {}, "frontmatter-format": {}, "index-frontmatter": {}, + "link-escape": {}, "link-target": {}, "log-date": {}, "log-frontmatter": {}, diff --git a/packages/cli/internal/okf/validation_rules.go b/packages/cli/internal/okf/validation_rules.go index ad02bfc..1ea6b08 100644 --- a/packages/cli/internal/okf/validation_rules.go +++ b/packages/cli/internal/okf/validation_rules.go @@ -140,7 +140,7 @@ func validateDocumentLinks(root string, document ASTDocument, result *Result) { result.Warnings = append(result.Warnings, Issue{ Path: document.Rel, Line: link.Line, - Rule: "link-target", + Rule: "link-escape", Message: fmt.Sprintf("link target escapes bundle root: %s", link.Href), }) continue