Skip to content

Add shared Markdown frontmatter checks to kennlint - #270

Open
salmonumbrella wants to merge 12 commits into
kenn-io:mainfrom
salmonumbrella:feat/kennlint-frontmatter
Open

salmonumbrella wants to merge 12 commits into
kenn-io:mainfrom
salmonumbrella:feat/kennlint-frontmatter

Conversation

@salmonumbrella

Copy link
Copy Markdown
Contributor

What changed

  • Add kennlint frontmatter for tracked Markdown, with strict metadata validation, configurable excludes, -fix, and merge-base edit checks.
  • Backfill kit's Markdown and wire the check into the Makefile, prek hooks, and PR CI.

Why

Missing metadata and edits without date updates currently pass the shared lint workflow. Repositories can now adopt one versioned check instead of maintaining their own scripts.

Usage

go run go.kenn.io/kit/cmd/kennlint@<kit version> frontmatter

Use -fix to backfill dates and -changed origin/main to require date changes for edited files. The adoption guide covers exclusions and hook setup.

@roborev-ci

roborev-ci Bot commented Nov 2, 2026

Copy link
Copy Markdown

roborev: Combined Review (fb83d6e)

Verdict: Changes require fixes for 2 findings.

Medium

  • cmd/kennlint/frontmatter.go:214; cmd/kennlint/frontmatter.go:226: Historical resolution does not follow intermediate directory symlinks. Valid Markdown links through such directories pass working-tree validation but cause -changed to abort, even when unchanged.

    Fix: Resolve intermediate symlink components against the historical tree, accumulate their change flags, preserve filesystem ordering for '..', and add a regression test.

  • cmd/kennlint/frontmatter_test.go:286: TestFrontmatterSymlinks fixes AGENTS.md before checking CLAUDE.md, so the latter requires no write. The test therefore passes even if WithFollowLink is removed and never exercises fixing through the symlink.

    Fix: Limit the fix invocation to CLAUDE.md, retain the link-preservation assertion, and verify that AGENTS.md contains the corrected date.


Reviewers: 2x codex, codex (security), codex (test-value) | Synthesis: codex, 19s | Total: 11m52s

@roborev-ci

roborev-ci Bot commented Nov 2, 2026

Copy link
Copy Markdown

roborev: Combined Review (2cf4807)

Verdict: Changes require fixes for 1 finding.

Medium

  • .github/workflows/ci.yml:60: The workflow accepts PRs targeting any branch but defaults to origin/main for edit-date comparisons. A content edit without a date update can pass when the target branch already changed that date since diverging from main.

    Fix: Set FRONTMATTER_BASE to github.event.pull_request.base.sha so enforcement uses the PR’s actual target.


Reviewers: 2x codex, codex (security), codex (test-value) | Synthesis: codex, 14s | Total: 8m47s

@roborev-ci

roborev-ci Bot commented Nov 2, 2026

Copy link
Copy Markdown

roborev: Combined Review (608eaba)

Verdict: Changes require fixes for 2 findings.

Medium

  • cmd/kennlint/frontmatter.go:543: On Windows checkouts that materialize tracked symlinks as regular files, validation reads link-target text as Markdown. Valid aliases fail validation, and -fix corrupts the tracked link contents by prepending frontmatter.

    Fix: Retain Git index modes and resolve symlink placeholders through their targets before validation or fixing. Add coverage that does not require OS symlink privileges.

  • cmd/kennlint/frontmatter.go:395: A Markdown symlink with an absolute target inside the repository passes ordinary validation, but -changed rejects its historical target even when the document and link are unchanged.

    Fix: Resolve repository-internal absolute targets into repository-relative historical paths while retaining the containment check. Add coverage for an unchanged internal absolute symlink with -changed.


Reviewers: 2x codex, codex (security), codex (test-value) | Synthesis: codex, 15s | Total: 8m5s

@salmonumbrella
salmonumbrella marked this pull request as ready for review November 2, 2026 19:33
@roborev-ci

roborev-ci Bot commented Nov 2, 2026

Copy link
Copy Markdown

roborev: Combined Review (125ca68)

Verdict: Changes require fixes for 2 findings.

Medium

  • cmd/kennlint/frontmatter.go:437: An unstaged replacement of a tracked Markdown symlink with a regular file is treated as a symlink placeholder even with core.symlinks=true. The resolver interprets the document as a target path, causing validation and -fix to abort.

    Fix: Restrict regular-file placeholder handling to checkouts with core.symlinks=false and add coverage for an unstaged symlink-to-regular-file replacement.

  • cmd/kennlint/frontmatter.go:737: Repairing a previously dangling Markdown symlink fails -changed validation because resolution of the historical target aborts validation despite the valid current target.

    Fix: Treat an unresolvable historical target as having no comparable previous metadata, while preserving errors for Git failures and invalid current targets. Add coverage for repairing a dangling link.


Reviewers: 2x codex, codex (security), codex (test-value) | Synthesis: codex, 12s | Total: 9m57s

@roborev-ci

roborev-ci Bot commented Nov 2, 2026

Copy link
Copy Markdown

roborev: Combined Review (25a3fc0)

Verdict: Changes require fixes for 4 findings.

Medium

  • cmd/kennlint/frontmatter.go:383: On Windows, symlink targets rooted without a volume, such as \repo\target.md, are treated as relative. Valid internal aliases can fail validation or resolve to an unintended nested file.

    Fix: Anchor Windows targets rooted without a volume to the repository’s volume before checking containment. Add placeholder tests for both separator forms.

  • cmd/kennlint/frontmatter.go:448: On Windows with core.symlinks=false, a chained placeholder referencing LINK.md fails when the tracked placeholder is named link.md. The case-sensitive indexModes lookup misses the symlink mode, causing target text to be read as Markdown.

    Fix: Use the filesystem’s case semantics for index-mode lookups before deciding whether a path is a placeholder. Cover chained placeholders with differing path case.

  • cmd/kennlint/frontmatter.go:539: Replacing an external Markdown symlink with a valid internal link or regular document still fails -changed because historical resolution returns an escape error instead of unavailable metadata.

    Fix: Classify external historical targets as unavailable metadata without following them. Preserve Git errors and rejection of current external targets, and test repairing an external link.

  • cmd/kennlint/frontmatter.go:559: Repairing an alias that historically targeted '.' makes -changed abort because terminal historical resolution returns an error without errFrontmatterHistoricalTargetUnavailable, bypassing the fallback for unavailable historical content.

    Fix: Wrap this terminal resolution failure with errFrontmatterHistoricalTargetUnavailable and test replacing a historical repository-directory target with a valid document.


Reviewers: 2x codex, codex (security), codex (test-value) | Synthesis: codex, 22s | Total: 11m37s

@roborev-ci

roborev-ci Bot commented Nov 2, 2026

Copy link
Copy Markdown

roborev: Combined Review (667d1bb)

Verdict: Changes require fixes for 2 findings.

Medium

  • cmd/kennlint/frontmatter.go:383: Volume-less Windows rooted targets such as \repo\target.md return before volume anchoring, causing aliases to resolve beneath the repository instead of the drive root. Validation and fixes can fail or reach an unintended file.

    Fix: Recognize Windows rooted targets before the relative-path early return and anchor them to the repository volume.

  • cmd/kennlint/frontmatter.go:550: Historical resolution uses case-sensitive tree lookups despite case-insensitive Windows working-path resolution. An alias targeting DOCUMENT.txt when Git tracks document.txt can therefore bypass last_edited validation under -changed because historical metadata appears unavailable and the non-Markdown target receives no separate check.

    Fix: Apply the working filesystem's case semantics to historical entry and directory lookups, preserve the matched entry's changed flag, and cover date checking through differently cased aliases.


Reviewers: 2x codex, codex (security), codex (test-value) | Synthesis: codex, 16s | Total: 8m13s

@roborev-ci

roborev-ci Bot commented Nov 2, 2026

Copy link
Copy Markdown

roborev: Combined Review (0e98da2)

Verdict: Changes require fixes for 4 findings.

Medium

  • cmd/kennlint/frontmatter.go:417: On case-insensitive macOS filesystems, an internal absolute symlink whose repository prefix differs in capitalization is incorrectly rejected as external because prefix comparison folds case only on Windows.

    Fix: Apply the repository’s case semantics when comparing absolute target prefixes, and test differently capitalized internal absolute targets.

  • cmd/kennlint/frontmatter.go:849: When the only working-tree edit quotes an existing date, -fix -changed restores the base content but still reports that content changed without changing last_edited. The unconditional didFix condition tracks whether a fix occurred instead of whether the final content differs.

    Fix: Compare the final fixed content against the base while preserving Git’s line-ending normalization, and test a quote-only edit that fixes back to the original content.

  • cmd/kennlint/frontmatter.go:519: On Windows, link targets containing parent components are traversed before normalization, potentially resolving to the wrong document. Validation can read that document and -fix can modify it. Historical resolution repeats this ordering.

    Fix: Normalize Windows destinations before traversing link targets in both working and historical resolution, preserve Unix traversal order, and test distinct files at both possible destinations.

  • cmd/kennlint/frontmatter.go:522: On Windows, a Markdown alias targeting a path through an internal NTFS junction fails with 'not a directory' because junctions appear as ModeIrregular and are missed by the symlink check.

    Fix: Recognize junctions using fslink.Classify/Readlink and use pathresolve.EvalSymlinks for final canonicalization, retaining repository containment checks. Test an internal junction target.


Reviewers: 2x codex, codex (security), codex (test-value) | Synthesis: codex, 15s | Total: 11m12s

@roborev-ci

roborev-ci Bot commented Nov 2, 2026

Copy link
Copy Markdown

roborev: Combined Review (f6862cb)

Verdict: Changes require fixes for 1 finding.

Medium

  • cmd/kennlint/frontmatter.go:585: On Windows with core.symlinks=false, traversing a junction to a tracked symlink placeholder uses the junction’s lexical path for index lookup. Validation reads the placeholder as Markdown, and -fix overwrites it with frontmatter, breaking the link.

    Fix: Resolve junction targets and use the canonical repository-relative path for index-mode lookups while preserving Windows traversal semantics. Cover validation and fixing through an internal junction to a placeholder.


Reviewers: 2x codex, codex (security), codex (test-value) | Synthesis: codex, 16s | Total: 10m45s

@roborev-ci

roborev-ci Bot commented Nov 2, 2026

Copy link
Copy Markdown

roborev: Combined Review (052462a)

Verdict: No findings at or above medium severity.


Reviewers: 2x codex, codex (security), codex (test-value) | Synthesis: codex, 13s | Total: 8m44s

@salmonumbrella

Copy link
Copy Markdown
Contributor Author

@mariusvniekerk I had to add last_edited for us 🤣

@roborev-ci

roborev-ci Bot commented Nov 3, 2026

Copy link
Copy Markdown

roborev: Combined Review (2122e76)

Verdict: Changes require fixes for 1 finding.

Medium

  • cmd/kennlint/frontmatter.go:447: Internal absolute symlinks are rejected when their repository prefix uses an equivalent filesystem spelling, such as /tmp/repo versus /private/tmp/repo on macOS. Raw prefix comparison incorrectly reports an external target.

    Fix: Canonicalize existing repository-prefix aliases before checking containment, preserving Unix symlink and parent traversal order.


Reviewers: 2x codex, codex (security), codex (test-value) | Synthesis: codex, 17s | Total: 13m55s

@mariusvniekerk

Copy link
Copy Markdown
Contributor

I honestly still do not see the value, what does this actually buy us? Git already has file dates, that's enough

@mariusvniekerk mariusvniekerk added the question Further information is requested label Nov 3, 2026
@salmonumbrella

salmonumbrella commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor Author

Agents reading a markdown doc to know if stale.

@mariusvniekerk mariusvniekerk self-assigned this Nov 4, 2026
@mariusvniekerk
mariusvniekerk force-pushed the feat/kennlint-frontmatter branch from 2122e76 to 08421d4 Compare November 4, 2026 21:33
@roborev-ci

roborev-ci Bot commented Nov 4, 2026

Copy link
Copy Markdown

roborev: Combined Review (08421d4)

Verdict: Changes require fixes for 1 finding.

Medium

  • cmd/kennlint/frontmatter.go:944: A tracked alias.md pointing to README.md allows frontmatter -fix to insert frontmatter into the excluded README because exclusions check only the alias path before writing to its resolved target.

    Fix: Apply protected-file exclusions to resolved targets before validation or mutation, and add a regression test for aliases to excluded files.


Reviewers: 3x codex, codex (security), codex (test-value) | Synthesis: codex, 13s | Total: 10m12s

mariusvniekerk and others added 2 commits November 4, 2026 23:02
The frontmatter check followed Markdown symlinks to their targets. To do
that safely it grew its own resolver for junctions, core.symlinks=false
placeholders, case-insensitive paths, and drive-rooted targets, plus a
second resolver for symlinks in the merge base. Each fix exposed another
edge case. The last one let -fix write frontmatter into an excluded
README through an alias.

A symlink needs no check of its own. When its target is in the
repository, Git tracks the target, and the command checks it under its
own path. The command now skips any Markdown file that the index records
as a symlink. The index records that mode even when Git checks links out
as plain files, so -fix can no longer write through a link.

-changed now asks git diff which files changed since the merge base,
after -fix runs. A fix that restores the base content is not an edit,
and Git still applies line-ending rules, so a CRLF checkout is not an
edit either.

Validation of the portable Agent Skills format is a separate concern
from document dates. The command now skips SKILL.md and its bundle
instead of checking them. A later change can add skill validation if a
repository needs it.

Generated with Claude Code (claude-opus-5-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Setting the executable bit on a Markdown file made -changed demand a
new last_edited date, although the text did not change. Git lists a
mode-only change as modified. Its line counts are zero, so the check now
uses them to tell a mode change from an edit.

A test also covers a tracked file that the working tree replaced with a
symlink, which -fix must leave alone.

Generated with Claude Code (claude-opus-5-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@mariusvniekerk

Copy link
Copy Markdown
Contributor

Note

🤖 Claude Opus 5.5 responding on behalf of @mariusvniekerk

The roborev finding on 08421d4 (an alias.md → README.md symlink lets -fix write frontmatter into the excluded README) no longer applies. 57cdd41 stops following Markdown symlinks:

  • The command skips any .md file whose index mode is 120000. A target inside the repository is tracked and checked under its own path. Git records that mode even when core.symlinks=false checks the link out as a plain file.
  • A tracked file that the working tree replaced with a symlink is also skipped, so -fix never writes through a link.
  • TestFrontmatterSkipsSymlinks covers the README alias, an external target, and a dangling link. TestFrontmatterSkipsSymlinkPlaceholders and TestFrontmatterSkipsUnstagedSymlink cover the other two cases.

This removes the symlink, junction, case-folding, and drive-root resolver that the earlier fix commits built up, about 450 lines. The same commit drops SKILL.md profile validation, so skill bundles are now skipped. ad56373 stops -changed from treating a mode-only change (such as chmod +x) as a content edit.

generated by a clanker

@roborev-ci

roborev-ci Bot commented Nov 4, 2026

Copy link
Copy Markdown

roborev: Combined Review (ad56373)

Verdict: Changes require fixes for 1 finding.

Medium

  • cmd/kennlint/frontmatter.go:261, cmd/kennlint/frontmatter.go:268: The symlink check inspects only the final path component. If a tracked directory is replaced by an unstaged symlink or junction, -fix can read and rewrite its destination files outside the repository; atomicfile.WriteFile also follows parent-directory links.

    Fix: Check repository-relative ancestor components for symlinks or junctions using fslink.Classify and skip affected files before reading or fixing them. Add regression coverage for an unstaged directory link.


Reviewers: 3x codex, codex (security), codex (test-value) | Synthesis: codex, 15s | Total: 5m7s

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

question Further information is requested

Development

Successfully merging this pull request may close these issues.

2 participants