Skip to content
Open
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
19 changes: 19 additions & 0 deletions Wiki/changelog/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion Wiki/features/commands/validate.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
50 changes: 50 additions & 0 deletions packages/cli/internal/okf/validate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/internal/okf/validation_checks.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
{
Expand Down
1 change: 1 addition & 0 deletions packages/cli/internal/okf/validation_policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ var knownValidationRules = map[string]struct{}{
"frontmatter": {},
"frontmatter-format": {},
"index-frontmatter": {},
"link-escape": {},
"link-target": {},
"log-date": {},
"log-frontmatter": {},
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/internal/okf/validation_rules.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down