From af89c3aa646c198cce4cd2b11fb98e076a757578 Mon Sep 17 00:00:00 2001 From: nvk Date: Mon, 24 Aug 2026 19:09:14 -0400 Subject: [PATCH] Fix raw source path linting without provenance false positives --- AGENTS.md | 4 +- claude-plugin/bin/llm-wiki | 77 +++++++++++++++++++ .../skills/wiki-manager/references/linting.md | 8 ++ .../wiki-manager/references/wiki-structure.md | 2 +- plugins/llm-wiki-opencode/bin/llm-wiki | 77 +++++++++++++++++++ plugins/llm-wiki/bin/llm-wiki | 77 +++++++++++++++++++ .../skills/wiki/references/linting.md | 8 ++ .../skills/wiki/references/wiki-structure.md | 2 +- scripts/llm-wiki | 77 +++++++++++++++++++ .../defects/raw-source-unresolved/_index.md | 36 +++++++++ .../defects/raw-source-unresolved/config.md | 15 ++++ .../raw-source-unresolved/datasets/_index.md | 18 +++++ .../bitcointalk-temporal-graph/MANIFEST.md | 48 ++++++++++++ .../bitcointalk-temporal-graph/_index.md | 17 ++++ .../profiles/_index.md | 10 +++ .../queries/_index.md | 10 +++ .../samples/_index.md | 10 +++ .../raw-source-unresolved/inventory/_index.md | 30 ++++++++ .../inventory/candidates/_index.md | 11 +++ .../candidates/bitcointalk-archive.md | 33 ++++++++ .../inventory/corpora/_index.md | 10 +++ .../inventory/entities/_index.md | 10 +++ .../inventory/items/_index.md | 11 +++ .../inventory/items/trx4m-ring-and-pinion.md | 44 +++++++++++ .../inventory/views/_index.md | 11 +++ .../inventory/views/active-actions.md | 18 +++++ .../defects/raw-source-unresolved/log.md | 10 +++ .../raw-source-unresolved/output/_index.md | 5 ++ .../output/sample-output.md | 12 +++ .../raw-source-unresolved/raw/_index.md | 20 +++++ .../raw/articles/2026-01-01-sample-article.md | 14 ++++ .../raw/articles/2026-01-02-second-article.md | 14 ++++ .../articles/2026-01-03-Title Cased Source.md | 14 ++++ .../raw/articles/_index.md | 7 ++ .../raw-source-unresolved/raw/data/_index.md | 4 + .../raw-source-unresolved/raw/notes/_index.md | 4 + .../raw/papers/2026-01-01-sample-paper.md | 14 ++++ .../raw/papers/_index.md | 5 ++ .../raw-source-unresolved/raw/repos/_index.md | 4 + .../defects/raw-source-unresolved/schema.md | 55 +++++++++++++ .../raw-source-unresolved/wiki/_index.md | 13 ++++ .../wiki/concepts/_index.md | 5 ++ .../wiki/concepts/sample-concept.md | 31 ++++++++ .../wiki/references/_index.md | 5 ++ .../wiki/references/sample-reference.md | 31 ++++++++ .../wiki/theses/_index.md | 4 + .../wiki/topics/_index.md | 4 + tests/generate-defect-fixtures.sh | 7 ++ tests/test-local-cli-lint.sh | 28 +++++++ tests/test-structure.sh | 6 ++ 50 files changed, 986 insertions(+), 4 deletions(-) create mode 100644 tests/fixtures/defects/raw-source-unresolved/_index.md create mode 100644 tests/fixtures/defects/raw-source-unresolved/config.md create mode 100644 tests/fixtures/defects/raw-source-unresolved/datasets/_index.md create mode 100644 tests/fixtures/defects/raw-source-unresolved/datasets/bitcointalk-temporal-graph/MANIFEST.md create mode 100644 tests/fixtures/defects/raw-source-unresolved/datasets/bitcointalk-temporal-graph/_index.md create mode 100644 tests/fixtures/defects/raw-source-unresolved/datasets/bitcointalk-temporal-graph/profiles/_index.md create mode 100644 tests/fixtures/defects/raw-source-unresolved/datasets/bitcointalk-temporal-graph/queries/_index.md create mode 100644 tests/fixtures/defects/raw-source-unresolved/datasets/bitcointalk-temporal-graph/samples/_index.md create mode 100644 tests/fixtures/defects/raw-source-unresolved/inventory/_index.md create mode 100644 tests/fixtures/defects/raw-source-unresolved/inventory/candidates/_index.md create mode 100644 tests/fixtures/defects/raw-source-unresolved/inventory/candidates/bitcointalk-archive.md create mode 100644 tests/fixtures/defects/raw-source-unresolved/inventory/corpora/_index.md create mode 100644 tests/fixtures/defects/raw-source-unresolved/inventory/entities/_index.md create mode 100644 tests/fixtures/defects/raw-source-unresolved/inventory/items/_index.md create mode 100644 tests/fixtures/defects/raw-source-unresolved/inventory/items/trx4m-ring-and-pinion.md create mode 100644 tests/fixtures/defects/raw-source-unresolved/inventory/views/_index.md create mode 100644 tests/fixtures/defects/raw-source-unresolved/inventory/views/active-actions.md create mode 100644 tests/fixtures/defects/raw-source-unresolved/log.md create mode 100644 tests/fixtures/defects/raw-source-unresolved/output/_index.md create mode 100644 tests/fixtures/defects/raw-source-unresolved/output/sample-output.md create mode 100644 tests/fixtures/defects/raw-source-unresolved/raw/_index.md create mode 100644 tests/fixtures/defects/raw-source-unresolved/raw/articles/2026-01-01-sample-article.md create mode 100644 tests/fixtures/defects/raw-source-unresolved/raw/articles/2026-01-02-second-article.md create mode 100644 tests/fixtures/defects/raw-source-unresolved/raw/articles/2026-01-03-Title Cased Source.md create mode 100644 tests/fixtures/defects/raw-source-unresolved/raw/articles/_index.md create mode 100644 tests/fixtures/defects/raw-source-unresolved/raw/data/_index.md create mode 100644 tests/fixtures/defects/raw-source-unresolved/raw/notes/_index.md create mode 100644 tests/fixtures/defects/raw-source-unresolved/raw/papers/2026-01-01-sample-paper.md create mode 100644 tests/fixtures/defects/raw-source-unresolved/raw/papers/_index.md create mode 100644 tests/fixtures/defects/raw-source-unresolved/raw/repos/_index.md create mode 100644 tests/fixtures/defects/raw-source-unresolved/schema.md create mode 100644 tests/fixtures/defects/raw-source-unresolved/wiki/_index.md create mode 100644 tests/fixtures/defects/raw-source-unresolved/wiki/concepts/_index.md create mode 100644 tests/fixtures/defects/raw-source-unresolved/wiki/concepts/sample-concept.md create mode 100644 tests/fixtures/defects/raw-source-unresolved/wiki/references/_index.md create mode 100644 tests/fixtures/defects/raw-source-unresolved/wiki/references/sample-reference.md create mode 100644 tests/fixtures/defects/raw-source-unresolved/wiki/theses/_index.md create mode 100644 tests/fixtures/defects/raw-source-unresolved/wiki/topics/_index.md diff --git a/AGENTS.md b/AGENTS.md index 84de5d4..1dccaa3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -211,7 +211,7 @@ established wikis and copy only the useful proposal parts into `schema.md`. ```yaml --- title: "Title" -source: "URL or filepath or MANUAL" +source: "URL, filepath, MANUAL, or provenance label such as session" type: articles|papers|repos|notes|data ingested: YYYY-MM-DD tags: [tag1, tag2] @@ -854,7 +854,7 @@ Health checks with auto-fix capability. Lint **is** the migration path — there path/status drift. Normal lint reports archived topics as skipped; `--include-archived` or `--archived-only` structurally maintains them. -**Checks**: structure integrity, frontmatter validity (plus legacy key/value aliases C13), canonical placement of raw/wiki files (C11), unknown-file quarantine for raw/wiki/inventory/datasets/root (C12), index consistency, link integrity, source provenance (dangling refs, unresolved retraction markers), tag hygiene, coverage, project `WHY.md` presence (C8a), project staleness via source chain (C8b), legacy `_project.md` migration to `WHY.md` (C8c), project candidates (C9), inventory migration candidates (C16), dataset migration candidates (C17), archive registry drift and active/archive collisions (C19), deep fact-checking (optional). +**Checks**: structure integrity, frontmatter validity (plus legacy key/value aliases C13), canonical placement of raw/wiki files (C11), unknown-file quarantine for raw/wiki/inventory/datasets/root (C12), index consistency, link integrity, source provenance (dangling refs, unresolved explicit local raw-source paths, unresolved retraction markers), tag hygiene, coverage, project `WHY.md` presence (C8a), project staleness via source chain (C8b), legacy `_project.md` migration to `WHY.md` (C8c), project candidates (C9), inventory migration candidates (C16), dataset migration candidates (C17), archive registry drift and active/archive collisions (C19), deep fact-checking (optional). **Auto-fix** (`--fix`): rewrite legacy frontmatter keys/values to canonical (C13), move misplaced raw/wiki files to their canonical directory (C11), quarantine unknown files to `inbox/.unknown` (C12), migrate legacy `_project.md` to `WHY.md` (C8c), add a default human-owned `schema.md` in advisory mode when missing, repair missing indexes inside existing inventory/dataset layers (C16/C17), repair unambiguous archive registry path/status drift (C19), missing indexes, orphan files, dead index entries, statistics mismatch, missing bidirectional links, empty frontmatter fields, dangling source references, regenerate projects-aware `output/_index.md`. Never auto-delete unknown directories. Never auto-create `WHY.md` with placeholder goals (C8a is warn-only — manufactured rationale is worse than missing). Never create completely absent optional inventory or dataset trees just to populate placeholders. Never auto-move files into projects (C9 is human-authored via `/wiki:project`). Never auto-migrate output artifacts into inventory or dataset records (C16/C17 are explicit via `/wiki:inventory migrate-output --apply` and `/wiki:dataset migrate-output --apply`). Never move topics into or out of archive during lint; archive/restore is explicit. On slug collisions during a placement move, skip and warn. diff --git a/claude-plugin/bin/llm-wiki b/claude-plugin/bin/llm-wiki index 0d8a936..304c2a0 100755 --- a/claude-plugin/bin/llm-wiki +++ b/claude-plugin/bin/llm-wiki @@ -60,6 +60,9 @@ CONFIDENCE_VALUES = {"high", "medium", "low"} VOLATILITY_VALUES = {"hot", "warm", "cold"} PERMISSION_DENIED_ERRNOS = {1, 13} WINDOWS_ABSOLUTE_PATH_RE = re.compile(r"^/?[A-Za-z]:[\\/]") +SCHEMELESS_WEB_SOURCE_RE = re.compile( + r"^(?:[A-Za-z0-9-]+\.)+[A-Za-z]{2,}(?::\d+)?(?:[/#?]|$)" +) ADAPTER_PROTOCOL = "llm-wiki-adapter/v1" ADAPTER_REGISTRY_SCHEMA = 1 ADAPTER_MANIFEST_NAME = ".llm-wiki-adapter.json" @@ -1473,6 +1476,16 @@ def check_source_provenance(ctx: LintContext) -> None: f"Inventory source reference does not resolve: {source}.", doc.path, ) + elif rel.parts[0] == "raw": + source = doc.frontmatter.get("source") + if source: + candidate = raw_source_path_candidate(ctx, doc.path, str(source)) + if candidate is not None and not source_path_exists(candidate): + ctx.issue( + "warning", + f"Raw source reference does not resolve: {source}.", + doc.path, + ) if "RETRACTED-SOURCE" in doc.body: ctx.issue("warning", "Retracted-source marker remains in the file body.", doc.path) @@ -1562,6 +1575,70 @@ def resolve_source_ref(ctx: LintContext, owner: Path, ref: str, wiki_source: boo return None +def raw_source_path_candidate(ctx: LintContext, owner: Path, ref: str) -> Path | None: + """Return a candidate only when a raw source value is clearly a local path. + + Raw provenance also uses sentinels such as ``session`` and descriptive + labels. Treating every non-HTTP value as a path would make those established + values fail lint. URI schemes remain external except for ``file://``; bare + relative paths must contain a separator and no whitespace to be considered + unambiguous. + """ + ref = strip_matching_quotes(ref.strip()) + if not ref: + return None + + windows_ref = windows_absolute_path(ref) + if windows_ref is not None: + return windows_ref + + try: + parsed = urllib.parse.urlsplit(ref) + except ValueError: + parsed = None + if parsed is not None and parsed.scheme: + # Do not mistake a Windows drive letter for a URI scheme on a platform + # where Windows paths are not native. + if WINDOWS_ABSOLUTE_PATH_RE.match(ref): + return Path(ref) + if parsed.scheme.lower() != "file": + return None + path_text = urllib.parse.unquote(parsed.path) + if parsed.netloc and parsed.netloc.lower() != "localhost": + path_text = f"//{parsed.netloc}{path_text}" + if os.name == "nt" and re.match(r"^/[A-Za-z]:/", path_text): + path_text = path_text[1:] + file_path = Path(path_text) + return file_path if file_path.is_absolute() else ctx.root / file_path + + if SCHEMELESS_WEB_SOURCE_RE.match(ref): + return None + + explicit_relative = ref.startswith(("../", "./", "..\\", ".\\")) + home_relative = ref == "~" or ref.startswith(("~/", "~\\")) + absolute = Path(ref).is_absolute() + unambiguous_bare_relative = ( + not any(char.isspace() for char in ref) and ("/" in ref or "\\" in ref) + ) + if not (explicit_relative or home_relative or absolute or unambiguous_bare_relative): + return None + + ref_path = expand_leading_tilde(ref) + if ref_path.is_absolute(): + return ref_path + if explicit_relative: + return owner.parent / ref_path + return ctx.root / ref_path + + +def source_path_exists(path: Path) -> bool: + """Check a source file or directory without crashing on sandbox denials.""" + try: + return path.exists() + except OSError: + return False + + def strip_matching_quotes(value: str) -> str: if (value.startswith('"') and value.endswith('"')) or ( value.startswith("'") and value.endswith("'") diff --git a/claude-plugin/skills/wiki-manager/references/linting.md b/claude-plugin/skills/wiki-manager/references/linting.md index bcc3f04..20311b1 100644 --- a/claude-plugin/skills/wiki-manager/references/linting.md +++ b/claude-plugin/skills/wiki-manager/references/linting.md @@ -102,6 +102,8 @@ There is no `/wiki:migrate` command and there should never be one. Lint rules ** - [ ] All markdown links `[text](path)` in wiki articles and inventory records resolve to existing local files when they are local paths +- [ ] Do not apply this check to raw source bodies. Imported Markdown may retain + relative links whose targets live only in the upstream source collection. - [ ] All "See Also" links are bidirectional (if A→B, then B→A) - [ ] All "Sources" links in wiki articles point to existing raw files. Links to paths with spaces should use angle-bracket markdown destinations, e.g. `[Title](<../../raw/articles/File Name.md>)`. @@ -112,6 +114,12 @@ There is no `/wiki:migrate` command and there should never be one. Lint rules ** existing files under `raw/`, `wiki/`, `output/`, `datasets/`, or `inventory/`. External URLs are allowed. Inventory provenance is operational state and must not be treated as factual evidence for compile/query/audit verdicts. +- [ ] Explicit local paths in a raw source's scalar `source:` field resolve to + an existing file or directory. Treat absolute paths, `file://` URIs, + `./`/`../`/`~/` paths, and whitespace-free relative paths containing a + directory separator as local. Other URI schemes, schemeless web URLs, + sentinels such as `MANUAL` or `session`, and free-form provenance labels are + not local-path checks. - [ ] No `` markers remain in article body (these should be resolved via `--recompile` or manual review) - [ ] No raw source file is referenced by zero wiki articles (orphan source — suggest compilation or removal) - [ ] Exempt raw files tagged `collection-manifest` from orphan-source warnings. A collection manifest is operational provenance for a batch import; child sources should be compiled, but the manifest itself does not need to appear in article `sources:`. diff --git a/claude-plugin/skills/wiki-manager/references/wiki-structure.md b/claude-plugin/skills/wiki-manager/references/wiki-structure.md index 8f4f1bb..edbde7a 100644 --- a/claude-plugin/skills/wiki-manager/references/wiki-structure.md +++ b/claude-plugin/skills/wiki-manager/references/wiki-structure.md @@ -372,7 +372,7 @@ Topic-guide helpers: ```markdown --- title: "Title" -source: "URL or filepath or MANUAL" +source: "URL, filepath, MANUAL, or provenance label such as session" type: articles|papers|repos|notes|data ingested: YYYY-MM-DD tags: [tag1, tag2] diff --git a/plugins/llm-wiki-opencode/bin/llm-wiki b/plugins/llm-wiki-opencode/bin/llm-wiki index 0d8a936..304c2a0 100755 --- a/plugins/llm-wiki-opencode/bin/llm-wiki +++ b/plugins/llm-wiki-opencode/bin/llm-wiki @@ -60,6 +60,9 @@ CONFIDENCE_VALUES = {"high", "medium", "low"} VOLATILITY_VALUES = {"hot", "warm", "cold"} PERMISSION_DENIED_ERRNOS = {1, 13} WINDOWS_ABSOLUTE_PATH_RE = re.compile(r"^/?[A-Za-z]:[\\/]") +SCHEMELESS_WEB_SOURCE_RE = re.compile( + r"^(?:[A-Za-z0-9-]+\.)+[A-Za-z]{2,}(?::\d+)?(?:[/#?]|$)" +) ADAPTER_PROTOCOL = "llm-wiki-adapter/v1" ADAPTER_REGISTRY_SCHEMA = 1 ADAPTER_MANIFEST_NAME = ".llm-wiki-adapter.json" @@ -1473,6 +1476,16 @@ def check_source_provenance(ctx: LintContext) -> None: f"Inventory source reference does not resolve: {source}.", doc.path, ) + elif rel.parts[0] == "raw": + source = doc.frontmatter.get("source") + if source: + candidate = raw_source_path_candidate(ctx, doc.path, str(source)) + if candidate is not None and not source_path_exists(candidate): + ctx.issue( + "warning", + f"Raw source reference does not resolve: {source}.", + doc.path, + ) if "RETRACTED-SOURCE" in doc.body: ctx.issue("warning", "Retracted-source marker remains in the file body.", doc.path) @@ -1562,6 +1575,70 @@ def resolve_source_ref(ctx: LintContext, owner: Path, ref: str, wiki_source: boo return None +def raw_source_path_candidate(ctx: LintContext, owner: Path, ref: str) -> Path | None: + """Return a candidate only when a raw source value is clearly a local path. + + Raw provenance also uses sentinels such as ``session`` and descriptive + labels. Treating every non-HTTP value as a path would make those established + values fail lint. URI schemes remain external except for ``file://``; bare + relative paths must contain a separator and no whitespace to be considered + unambiguous. + """ + ref = strip_matching_quotes(ref.strip()) + if not ref: + return None + + windows_ref = windows_absolute_path(ref) + if windows_ref is not None: + return windows_ref + + try: + parsed = urllib.parse.urlsplit(ref) + except ValueError: + parsed = None + if parsed is not None and parsed.scheme: + # Do not mistake a Windows drive letter for a URI scheme on a platform + # where Windows paths are not native. + if WINDOWS_ABSOLUTE_PATH_RE.match(ref): + return Path(ref) + if parsed.scheme.lower() != "file": + return None + path_text = urllib.parse.unquote(parsed.path) + if parsed.netloc and parsed.netloc.lower() != "localhost": + path_text = f"//{parsed.netloc}{path_text}" + if os.name == "nt" and re.match(r"^/[A-Za-z]:/", path_text): + path_text = path_text[1:] + file_path = Path(path_text) + return file_path if file_path.is_absolute() else ctx.root / file_path + + if SCHEMELESS_WEB_SOURCE_RE.match(ref): + return None + + explicit_relative = ref.startswith(("../", "./", "..\\", ".\\")) + home_relative = ref == "~" or ref.startswith(("~/", "~\\")) + absolute = Path(ref).is_absolute() + unambiguous_bare_relative = ( + not any(char.isspace() for char in ref) and ("/" in ref or "\\" in ref) + ) + if not (explicit_relative or home_relative or absolute or unambiguous_bare_relative): + return None + + ref_path = expand_leading_tilde(ref) + if ref_path.is_absolute(): + return ref_path + if explicit_relative: + return owner.parent / ref_path + return ctx.root / ref_path + + +def source_path_exists(path: Path) -> bool: + """Check a source file or directory without crashing on sandbox denials.""" + try: + return path.exists() + except OSError: + return False + + def strip_matching_quotes(value: str) -> str: if (value.startswith('"') and value.endswith('"')) or ( value.startswith("'") and value.endswith("'") diff --git a/plugins/llm-wiki/bin/llm-wiki b/plugins/llm-wiki/bin/llm-wiki index 0d8a936..304c2a0 100755 --- a/plugins/llm-wiki/bin/llm-wiki +++ b/plugins/llm-wiki/bin/llm-wiki @@ -60,6 +60,9 @@ CONFIDENCE_VALUES = {"high", "medium", "low"} VOLATILITY_VALUES = {"hot", "warm", "cold"} PERMISSION_DENIED_ERRNOS = {1, 13} WINDOWS_ABSOLUTE_PATH_RE = re.compile(r"^/?[A-Za-z]:[\\/]") +SCHEMELESS_WEB_SOURCE_RE = re.compile( + r"^(?:[A-Za-z0-9-]+\.)+[A-Za-z]{2,}(?::\d+)?(?:[/#?]|$)" +) ADAPTER_PROTOCOL = "llm-wiki-adapter/v1" ADAPTER_REGISTRY_SCHEMA = 1 ADAPTER_MANIFEST_NAME = ".llm-wiki-adapter.json" @@ -1473,6 +1476,16 @@ def check_source_provenance(ctx: LintContext) -> None: f"Inventory source reference does not resolve: {source}.", doc.path, ) + elif rel.parts[0] == "raw": + source = doc.frontmatter.get("source") + if source: + candidate = raw_source_path_candidate(ctx, doc.path, str(source)) + if candidate is not None and not source_path_exists(candidate): + ctx.issue( + "warning", + f"Raw source reference does not resolve: {source}.", + doc.path, + ) if "RETRACTED-SOURCE" in doc.body: ctx.issue("warning", "Retracted-source marker remains in the file body.", doc.path) @@ -1562,6 +1575,70 @@ def resolve_source_ref(ctx: LintContext, owner: Path, ref: str, wiki_source: boo return None +def raw_source_path_candidate(ctx: LintContext, owner: Path, ref: str) -> Path | None: + """Return a candidate only when a raw source value is clearly a local path. + + Raw provenance also uses sentinels such as ``session`` and descriptive + labels. Treating every non-HTTP value as a path would make those established + values fail lint. URI schemes remain external except for ``file://``; bare + relative paths must contain a separator and no whitespace to be considered + unambiguous. + """ + ref = strip_matching_quotes(ref.strip()) + if not ref: + return None + + windows_ref = windows_absolute_path(ref) + if windows_ref is not None: + return windows_ref + + try: + parsed = urllib.parse.urlsplit(ref) + except ValueError: + parsed = None + if parsed is not None and parsed.scheme: + # Do not mistake a Windows drive letter for a URI scheme on a platform + # where Windows paths are not native. + if WINDOWS_ABSOLUTE_PATH_RE.match(ref): + return Path(ref) + if parsed.scheme.lower() != "file": + return None + path_text = urllib.parse.unquote(parsed.path) + if parsed.netloc and parsed.netloc.lower() != "localhost": + path_text = f"//{parsed.netloc}{path_text}" + if os.name == "nt" and re.match(r"^/[A-Za-z]:/", path_text): + path_text = path_text[1:] + file_path = Path(path_text) + return file_path if file_path.is_absolute() else ctx.root / file_path + + if SCHEMELESS_WEB_SOURCE_RE.match(ref): + return None + + explicit_relative = ref.startswith(("../", "./", "..\\", ".\\")) + home_relative = ref == "~" or ref.startswith(("~/", "~\\")) + absolute = Path(ref).is_absolute() + unambiguous_bare_relative = ( + not any(char.isspace() for char in ref) and ("/" in ref or "\\" in ref) + ) + if not (explicit_relative or home_relative or absolute or unambiguous_bare_relative): + return None + + ref_path = expand_leading_tilde(ref) + if ref_path.is_absolute(): + return ref_path + if explicit_relative: + return owner.parent / ref_path + return ctx.root / ref_path + + +def source_path_exists(path: Path) -> bool: + """Check a source file or directory without crashing on sandbox denials.""" + try: + return path.exists() + except OSError: + return False + + def strip_matching_quotes(value: str) -> str: if (value.startswith('"') and value.endswith('"')) or ( value.startswith("'") and value.endswith("'") diff --git a/plugins/llm-wiki/skills/wiki/references/linting.md b/plugins/llm-wiki/skills/wiki/references/linting.md index bcc3f04..20311b1 100644 --- a/plugins/llm-wiki/skills/wiki/references/linting.md +++ b/plugins/llm-wiki/skills/wiki/references/linting.md @@ -102,6 +102,8 @@ There is no `/wiki:migrate` command and there should never be one. Lint rules ** - [ ] All markdown links `[text](path)` in wiki articles and inventory records resolve to existing local files when they are local paths +- [ ] Do not apply this check to raw source bodies. Imported Markdown may retain + relative links whose targets live only in the upstream source collection. - [ ] All "See Also" links are bidirectional (if A→B, then B→A) - [ ] All "Sources" links in wiki articles point to existing raw files. Links to paths with spaces should use angle-bracket markdown destinations, e.g. `[Title](<../../raw/articles/File Name.md>)`. @@ -112,6 +114,12 @@ There is no `/wiki:migrate` command and there should never be one. Lint rules ** existing files under `raw/`, `wiki/`, `output/`, `datasets/`, or `inventory/`. External URLs are allowed. Inventory provenance is operational state and must not be treated as factual evidence for compile/query/audit verdicts. +- [ ] Explicit local paths in a raw source's scalar `source:` field resolve to + an existing file or directory. Treat absolute paths, `file://` URIs, + `./`/`../`/`~/` paths, and whitespace-free relative paths containing a + directory separator as local. Other URI schemes, schemeless web URLs, + sentinels such as `MANUAL` or `session`, and free-form provenance labels are + not local-path checks. - [ ] No `` markers remain in article body (these should be resolved via `--recompile` or manual review) - [ ] No raw source file is referenced by zero wiki articles (orphan source — suggest compilation or removal) - [ ] Exempt raw files tagged `collection-manifest` from orphan-source warnings. A collection manifest is operational provenance for a batch import; child sources should be compiled, but the manifest itself does not need to appear in article `sources:`. diff --git a/plugins/llm-wiki/skills/wiki/references/wiki-structure.md b/plugins/llm-wiki/skills/wiki/references/wiki-structure.md index 8f4f1bb..edbde7a 100644 --- a/plugins/llm-wiki/skills/wiki/references/wiki-structure.md +++ b/plugins/llm-wiki/skills/wiki/references/wiki-structure.md @@ -372,7 +372,7 @@ Topic-guide helpers: ```markdown --- title: "Title" -source: "URL or filepath or MANUAL" +source: "URL, filepath, MANUAL, or provenance label such as session" type: articles|papers|repos|notes|data ingested: YYYY-MM-DD tags: [tag1, tag2] diff --git a/scripts/llm-wiki b/scripts/llm-wiki index 0d8a936..304c2a0 100755 --- a/scripts/llm-wiki +++ b/scripts/llm-wiki @@ -60,6 +60,9 @@ CONFIDENCE_VALUES = {"high", "medium", "low"} VOLATILITY_VALUES = {"hot", "warm", "cold"} PERMISSION_DENIED_ERRNOS = {1, 13} WINDOWS_ABSOLUTE_PATH_RE = re.compile(r"^/?[A-Za-z]:[\\/]") +SCHEMELESS_WEB_SOURCE_RE = re.compile( + r"^(?:[A-Za-z0-9-]+\.)+[A-Za-z]{2,}(?::\d+)?(?:[/#?]|$)" +) ADAPTER_PROTOCOL = "llm-wiki-adapter/v1" ADAPTER_REGISTRY_SCHEMA = 1 ADAPTER_MANIFEST_NAME = ".llm-wiki-adapter.json" @@ -1473,6 +1476,16 @@ def check_source_provenance(ctx: LintContext) -> None: f"Inventory source reference does not resolve: {source}.", doc.path, ) + elif rel.parts[0] == "raw": + source = doc.frontmatter.get("source") + if source: + candidate = raw_source_path_candidate(ctx, doc.path, str(source)) + if candidate is not None and not source_path_exists(candidate): + ctx.issue( + "warning", + f"Raw source reference does not resolve: {source}.", + doc.path, + ) if "RETRACTED-SOURCE" in doc.body: ctx.issue("warning", "Retracted-source marker remains in the file body.", doc.path) @@ -1562,6 +1575,70 @@ def resolve_source_ref(ctx: LintContext, owner: Path, ref: str, wiki_source: boo return None +def raw_source_path_candidate(ctx: LintContext, owner: Path, ref: str) -> Path | None: + """Return a candidate only when a raw source value is clearly a local path. + + Raw provenance also uses sentinels such as ``session`` and descriptive + labels. Treating every non-HTTP value as a path would make those established + values fail lint. URI schemes remain external except for ``file://``; bare + relative paths must contain a separator and no whitespace to be considered + unambiguous. + """ + ref = strip_matching_quotes(ref.strip()) + if not ref: + return None + + windows_ref = windows_absolute_path(ref) + if windows_ref is not None: + return windows_ref + + try: + parsed = urllib.parse.urlsplit(ref) + except ValueError: + parsed = None + if parsed is not None and parsed.scheme: + # Do not mistake a Windows drive letter for a URI scheme on a platform + # where Windows paths are not native. + if WINDOWS_ABSOLUTE_PATH_RE.match(ref): + return Path(ref) + if parsed.scheme.lower() != "file": + return None + path_text = urllib.parse.unquote(parsed.path) + if parsed.netloc and parsed.netloc.lower() != "localhost": + path_text = f"//{parsed.netloc}{path_text}" + if os.name == "nt" and re.match(r"^/[A-Za-z]:/", path_text): + path_text = path_text[1:] + file_path = Path(path_text) + return file_path if file_path.is_absolute() else ctx.root / file_path + + if SCHEMELESS_WEB_SOURCE_RE.match(ref): + return None + + explicit_relative = ref.startswith(("../", "./", "..\\", ".\\")) + home_relative = ref == "~" or ref.startswith(("~/", "~\\")) + absolute = Path(ref).is_absolute() + unambiguous_bare_relative = ( + not any(char.isspace() for char in ref) and ("/" in ref or "\\" in ref) + ) + if not (explicit_relative or home_relative or absolute or unambiguous_bare_relative): + return None + + ref_path = expand_leading_tilde(ref) + if ref_path.is_absolute(): + return ref_path + if explicit_relative: + return owner.parent / ref_path + return ctx.root / ref_path + + +def source_path_exists(path: Path) -> bool: + """Check a source file or directory without crashing on sandbox denials.""" + try: + return path.exists() + except OSError: + return False + + def strip_matching_quotes(value: str) -> str: if (value.startswith('"') and value.endswith('"')) or ( value.startswith("'") and value.endswith("'") diff --git a/tests/fixtures/defects/raw-source-unresolved/_index.md b/tests/fixtures/defects/raw-source-unresolved/_index.md new file mode 100644 index 0000000..4852262 --- /dev/null +++ b/tests/fixtures/defects/raw-source-unresolved/_index.md @@ -0,0 +1,36 @@ +# Test Wiki + +Research on testing patterns for validation purposes. + +## Stats +- Articles: 2 +- Sources: 4 +- Inventory records: 2 +- Datasets: 1 +- Last updated: 2026-01-03 + +## Articles + +| Article | Summary | +|---------|---------| +| [Sample Concept](wiki/concepts/sample-concept.md) | A concept about testing patterns synthesized from two sources | +| [Sample Reference](wiki/references/sample-reference.md) | Reference listing of testing tools and frameworks | + +## Outputs + +| Output | Type | Date | +|--------|------|------| + +## Raw Sources + +See [raw/_index.md](raw/_index.md) for 4 ingested sources. + +## Quick Navigation + +- [All Sources](raw/_index.md) +- [Inventory](inventory/_index.md) +- [Datasets](datasets/_index.md) +- [Concepts](wiki/concepts/_index.md) +- [Topics](wiki/topics/_index.md) +- [References](wiki/references/_index.md) +- [Outputs](output/_index.md) diff --git a/tests/fixtures/defects/raw-source-unresolved/config.md b/tests/fixtures/defects/raw-source-unresolved/config.md new file mode 100644 index 0000000..7a76f2c --- /dev/null +++ b/tests/fixtures/defects/raw-source-unresolved/config.md @@ -0,0 +1,15 @@ +--- +title: "Test Wiki" +description: "A minimal wiki fixture for structural validation testing" +created: 2026-01-01 +--- + +# Wiki Configuration + +## Scope + +Testing patterns and frameworks for LLM-powered tools. + +## Conventions + +Standard wiki conventions apply. diff --git a/tests/fixtures/defects/raw-source-unresolved/datasets/_index.md b/tests/fixtures/defects/raw-source-unresolved/datasets/_index.md new file mode 100644 index 0000000..bee14be --- /dev/null +++ b/tests/fixtures/defects/raw-source-unresolved/datasets/_index.md @@ -0,0 +1,18 @@ +# Dataset Registry Index + +> Dataset manifests for large or external data indexed by this wiki. + +Last updated: 2026-01-03 + +## Statistics + +- Datasets: 1 +- Active: 0 +- External: 1 +- Unavailable: 0 + +## Contents + +| Dataset | Status | Storage | Formats | Size | Records | Updated | +|---------|--------|---------|---------|------|---------|---------| +| [Bitcointalk Temporal Graph](bitcointalk-temporal-graph/MANIFEST.md) | external | external | csv, zip | unknown | unknown | 2026-01-03 | diff --git a/tests/fixtures/defects/raw-source-unresolved/datasets/bitcointalk-temporal-graph/MANIFEST.md b/tests/fixtures/defects/raw-source-unresolved/datasets/bitcointalk-temporal-graph/MANIFEST.md new file mode 100644 index 0000000..9addf3b --- /dev/null +++ b/tests/fixtures/defects/raw-source-unresolved/datasets/bitcointalk-temporal-graph/MANIFEST.md @@ -0,0 +1,48 @@ +--- +title: "Bitcointalk Temporal Graph" +dataset_id: bitcointalk-temporal-graph +status: external +storage: external +locations: + - https://figshare.com/articles/dataset/BitcoinTemporalGraph/26305093 +formats: [csv, zip] +size_bytes: null +record_count: null +schema_status: unknown +created: 2026-01-03 +updated: 2026-01-03 +tags: [bitcoin, bitcointalk, graph-dataset] +summary: "A sample external dataset manifest used to validate the dataset registry." +origin: output/sample-output.md +inventory: + - inventory/candidates/bitcointalk-archive.md +license: unknown +access: public +--- + +# Bitcointalk Temporal Graph + +## Scope + +External dataset fixture for validating wiki dataset manifests. + +## Storage Locations + +The sample manifest points at an external Figshare URL. The fixture does not +store dataset contents in the wiki. + +## Schema + +Unknown. + +## Samples And Profiles + +No samples or profiles are recorded in this fixture. + +## Query Recipes + +No query recipes are recorded in this fixture. + +## Caveats + +Fixture data only. diff --git a/tests/fixtures/defects/raw-source-unresolved/datasets/bitcointalk-temporal-graph/_index.md b/tests/fixtures/defects/raw-source-unresolved/datasets/bitcointalk-temporal-graph/_index.md new file mode 100644 index 0000000..71c6e40 --- /dev/null +++ b/tests/fixtures/defects/raw-source-unresolved/datasets/bitcointalk-temporal-graph/_index.md @@ -0,0 +1,17 @@ +# Bitcointalk Temporal Graph Dataset Index + +> Manifest, samples, profiles, and query recipes for this dataset. + +Last updated: 2026-01-03 + +## Contents + +| File | Summary | Tags | Updated | +|------|---------|------|---------| +| [MANIFEST.md](MANIFEST.md) | A sample external dataset manifest used to validate the dataset registry. | bitcoin, bitcointalk, graph-dataset | 2026-01-03 | + +## Quick Navigation + +- [Samples](samples/_index.md) +- [Profiles](profiles/_index.md) +- [Queries](queries/_index.md) diff --git a/tests/fixtures/defects/raw-source-unresolved/datasets/bitcointalk-temporal-graph/profiles/_index.md b/tests/fixtures/defects/raw-source-unresolved/datasets/bitcointalk-temporal-graph/profiles/_index.md new file mode 100644 index 0000000..08999ad --- /dev/null +++ b/tests/fixtures/defects/raw-source-unresolved/datasets/bitcointalk-temporal-graph/profiles/_index.md @@ -0,0 +1,10 @@ +# Bitcointalk Temporal Graph Profiles + +> Lightweight profiles for this dataset. + +Last updated: 2026-01-03 + +## Contents + +| File | Summary | Tags | Updated | +|------|---------|------|---------| diff --git a/tests/fixtures/defects/raw-source-unresolved/datasets/bitcointalk-temporal-graph/queries/_index.md b/tests/fixtures/defects/raw-source-unresolved/datasets/bitcointalk-temporal-graph/queries/_index.md new file mode 100644 index 0000000..0b94bb4 --- /dev/null +++ b/tests/fixtures/defects/raw-source-unresolved/datasets/bitcointalk-temporal-graph/queries/_index.md @@ -0,0 +1,10 @@ +# Bitcointalk Temporal Graph Queries + +> Read-only query recipes for this dataset. + +Last updated: 2026-01-03 + +## Contents + +| File | Summary | Tags | Updated | +|------|---------|------|---------| diff --git a/tests/fixtures/defects/raw-source-unresolved/datasets/bitcointalk-temporal-graph/samples/_index.md b/tests/fixtures/defects/raw-source-unresolved/datasets/bitcointalk-temporal-graph/samples/_index.md new file mode 100644 index 0000000..7d83879 --- /dev/null +++ b/tests/fixtures/defects/raw-source-unresolved/datasets/bitcointalk-temporal-graph/samples/_index.md @@ -0,0 +1,10 @@ +# Bitcointalk Temporal Graph Samples + +> Tiny samples or sampling recipes for this dataset. + +Last updated: 2026-01-03 + +## Contents + +| File | Summary | Tags | Updated | +|------|---------|------|---------| diff --git a/tests/fixtures/defects/raw-source-unresolved/inventory/_index.md b/tests/fixtures/defects/raw-source-unresolved/inventory/_index.md new file mode 100644 index 0000000..228ca52 --- /dev/null +++ b/tests/fixtures/defects/raw-source-unresolved/inventory/_index.md @@ -0,0 +1,30 @@ +# Inventory Index + +> Durable tracking records for items, candidates, entities, corpora, and watch items. + +Last updated: 2026-01-03 + +## Statistics + +- Total records: 2 +- Items: 1 +- Candidates: 1 +- Entities: 0 +- Corpora: 0 +- Active: 0 +- Blocked: 0 + +## Quick Navigation + +- [Items](items/_index.md) +- [Candidates](candidates/_index.md) +- [Entities](entities/_index.md) +- [Corpora](corpora/_index.md) +- [Views](views/_index.md) + +## Contents + +| File | Kind | Status | Priority | Next Action | Updated | +|------|------|--------|----------|-------------|---------| +| [trx4m-ring-and-pinion.md](items/trx4m-ring-and-pinion.md) | item | proposed | p1 | Decide whether this is the selected drivetrain default. | 2026-01-03 | +| [bitcointalk-archive.md](candidates/bitcointalk-archive.md) | ingest-candidate | proposed | p1 | Profile available archive formats before ingestion. | 2026-01-03 | diff --git a/tests/fixtures/defects/raw-source-unresolved/inventory/candidates/_index.md b/tests/fixtures/defects/raw-source-unresolved/inventory/candidates/_index.md new file mode 100644 index 0000000..9172a2a --- /dev/null +++ b/tests/fixtures/defects/raw-source-unresolved/inventory/candidates/_index.md @@ -0,0 +1,11 @@ +# Candidates Inventory + +> Ingest candidates, open questions, tasks, watch items, and proposed follow-up work. + +Last updated: 2026-01-03 + +## Contents + +| File | Kind | Status | Priority | Next Action | Updated | +|------|------|--------|----------|-------------|---------| +| [bitcointalk-archive.md](bitcointalk-archive.md) | ingest-candidate | proposed | p1 | Profile available archive formats before ingestion. | 2026-01-03 | diff --git a/tests/fixtures/defects/raw-source-unresolved/inventory/candidates/bitcointalk-archive.md b/tests/fixtures/defects/raw-source-unresolved/inventory/candidates/bitcointalk-archive.md new file mode 100644 index 0000000..425018c --- /dev/null +++ b/tests/fixtures/defects/raw-source-unresolved/inventory/candidates/bitcointalk-archive.md @@ -0,0 +1,33 @@ +--- +title: "Bitcointalk Archive" +kind: ingest-candidate +status: proposed +priority: p1 +created: 2026-01-03 +updated: 2026-01-03 +last_checked: 2026-01-03 +next_action: "Profile available archive formats before ingestion." +sources: + - output/sample-output.md +tags: [bitcoin, archive, ingest-candidate] +summary: "A sample ingest candidate used to validate inventory structure." +--- + +# Bitcointalk Archive + +## Why Track This + +This fixture represents a source queue item that should persist without being +treated as a raw source or compiled wiki article. + +## Current State + +Proposed for future ingestion. + +## Next Action + +Profile available archive formats before ingestion. + +## Related + +- [Sample output](../../output/sample-output.md) diff --git a/tests/fixtures/defects/raw-source-unresolved/inventory/corpora/_index.md b/tests/fixtures/defects/raw-source-unresolved/inventory/corpora/_index.md new file mode 100644 index 0000000..15d2c55 --- /dev/null +++ b/tests/fixtures/defects/raw-source-unresolved/inventory/corpora/_index.md @@ -0,0 +1,10 @@ +# Corpora Inventory + +> Source collections, archives, datasets, forums, document sets, and other bounded bodies of material. + +Last updated: 2026-01-03 + +## Contents + +| File | Kind | Status | Priority | Next Action | Updated | +|------|------|--------|----------|-------------|---------| diff --git a/tests/fixtures/defects/raw-source-unresolved/inventory/entities/_index.md b/tests/fixtures/defects/raw-source-unresolved/inventory/entities/_index.md new file mode 100644 index 0000000..7c4c346 --- /dev/null +++ b/tests/fixtures/defects/raw-source-unresolved/inventory/entities/_index.md @@ -0,0 +1,10 @@ +# Entities Inventory + +> People, organizations, projects, venues, standards bodies, and other named things worth tracking. + +Last updated: 2026-01-03 + +## Contents + +| File | Kind | Status | Priority | Next Action | Updated | +|------|------|--------|----------|-------------|---------| diff --git a/tests/fixtures/defects/raw-source-unresolved/inventory/items/_index.md b/tests/fixtures/defects/raw-source-unresolved/inventory/items/_index.md new file mode 100644 index 0000000..c6362a8 --- /dev/null +++ b/tests/fixtures/defects/raw-source-unresolved/inventory/items/_index.md @@ -0,0 +1,11 @@ +# Items Inventory + +> Physical and digital inventory items, parts, tools, assets, hosts, products, and SKUs. + +Last updated: 2026-01-03 + +## Contents + +| File | Kind | Status | Priority | Next Action | Updated | +|------|------|--------|----------|-------------|---------| +| [trx4m-ring-and-pinion.md](trx4m-ring-and-pinion.md) | item | proposed | p1 | Decide whether this is the selected drivetrain default. | 2026-01-03 | diff --git a/tests/fixtures/defects/raw-source-unresolved/inventory/items/trx4m-ring-and-pinion.md b/tests/fixtures/defects/raw-source-unresolved/inventory/items/trx4m-ring-and-pinion.md new file mode 100644 index 0000000..303f56a --- /dev/null +++ b/tests/fixtures/defects/raw-source-unresolved/inventory/items/trx4m-ring-and-pinion.md @@ -0,0 +1,44 @@ +--- +title: "TRX-4M Ring And Pinion" +kind: item +status: proposed +priority: p1 +created: 2026-01-03 +updated: 2026-01-03 +last_checked: 2026-01-03 +category: drivetrain +quantity: 1 +unit: set +state: selected +default_choice: "Treal hardened helical 12T/24T" +alternatives: + - Traxxas TRA9779 + - MEUS + - LGRP + - Injora alloy +needed_for: trx4m-build +next_action: "Decide whether this is the selected drivetrain default." +sources: + - wiki/references/sample-reference.md +tags: [trx4m, parts, drivetrain, item] +summary: "Sample physical part inventory record used to validate item inventory structure." +--- + +# TRX-4M Ring And Pinion + +## Why Track This + +This fixture represents an actual physical part or SKU that belongs in +inventory rather than in a generic candidate queue. + +## Current State + +Proposed as the selected drivetrain default for a TRX-4M build. + +## Next Action + +Decide whether this is the selected drivetrain default. + +## Related + +- [Sample reference](../../wiki/references/sample-reference.md) diff --git a/tests/fixtures/defects/raw-source-unresolved/inventory/views/_index.md b/tests/fixtures/defects/raw-source-unresolved/inventory/views/_index.md new file mode 100644 index 0000000..bb985f0 --- /dev/null +++ b/tests/fixtures/defects/raw-source-unresolved/inventory/views/_index.md @@ -0,0 +1,11 @@ +# Inventory Views + +> Generated inventory views and filtered lists. + +Last updated: 2026-01-03 + +## Contents + +| File | Summary | Tags | Updated | +|------|---------|------|---------| +| [active-actions.md](active-actions.md) | Derived chat-friendly table of active inventory records with next actions. | inventory, actions, view | 2026-01-03 | diff --git a/tests/fixtures/defects/raw-source-unresolved/inventory/views/active-actions.md b/tests/fixtures/defects/raw-source-unresolved/inventory/views/active-actions.md new file mode 100644 index 0000000..8682ab0 --- /dev/null +++ b/tests/fixtures/defects/raw-source-unresolved/inventory/views/active-actions.md @@ -0,0 +1,18 @@ +--- +title: "Active Inventory Actions" +view: actions +filters: + status: active +updated: 2026-01-03 +tags: [inventory, actions, view] +summary: "Derived chat-friendly table of active inventory records with next actions." +--- + +# Active Inventory Actions + +Generated from inventory record frontmatter on 2026-01-03. + +| Record | Kind | Priority | Next Action | Updated | +|--------|------|----------|-------------|---------| + +No active records in this fixture. diff --git a/tests/fixtures/defects/raw-source-unresolved/log.md b/tests/fixtures/defects/raw-source-unresolved/log.md new file mode 100644 index 0000000..12ff56c --- /dev/null +++ b/tests/fixtures/defects/raw-source-unresolved/log.md @@ -0,0 +1,10 @@ +# Wiki Activity Log + +## [2026-01-01] init | Wiki initialized +## [2026-01-01] ingest | Sample Article Source (raw/articles/2026-01-01-sample-article.md) +## [2026-01-01] ingest | Sample Paper Source (raw/papers/2026-01-01-sample-paper.md) +## [2026-01-02] ingest | Second Article Source (raw/articles/2026-01-02-second-article.md) +## [2026-01-03] ingest | Title Cased Source (raw/articles/2026-01-03-Title Cased Source.md) +## [2026-01-03] compile | 4 sources → 2 articles (sample-concept, sample-reference) + +## [2026-01-10] schema | created default advisory schema.md diff --git a/tests/fixtures/defects/raw-source-unresolved/output/_index.md b/tests/fixtures/defects/raw-source-unresolved/output/_index.md new file mode 100644 index 0000000..cd6bafc --- /dev/null +++ b/tests/fixtures/defects/raw-source-unresolved/output/_index.md @@ -0,0 +1,5 @@ +# Output Artifacts + +| Output | Type | Date | +|--------|------|------| +| [Sample Output](sample-output.md) | report | 2026-01-03 | diff --git a/tests/fixtures/defects/raw-source-unresolved/output/sample-output.md b/tests/fixtures/defects/raw-source-unresolved/output/sample-output.md new file mode 100644 index 0000000..7c8a9e1 --- /dev/null +++ b/tests/fixtures/defects/raw-source-unresolved/output/sample-output.md @@ -0,0 +1,12 @@ +--- +title: "Sample Output" +type: report +created: 2026-01-03 +updated: 2026-01-03 +summary: "Sample output artifact used by inventory provenance tests." +--- + +# Sample Output + +This fixture exists so inventory records can point at a generated artifact and +structural tests can verify local inventory `sources:` paths. diff --git a/tests/fixtures/defects/raw-source-unresolved/raw/_index.md b/tests/fixtures/defects/raw-source-unresolved/raw/_index.md new file mode 100644 index 0000000..3a93de3 --- /dev/null +++ b/tests/fixtures/defects/raw-source-unresolved/raw/_index.md @@ -0,0 +1,20 @@ +# Raw Sources Index + +## Articles +See [articles/_index.md](articles/_index.md) + +## Papers +See [papers/_index.md](papers/_index.md) + +## Repos +See [repos/_index.md](repos/_index.md) + +## Notes +See [notes/_index.md](notes/_index.md) + +## Data +See [data/_index.md](data/_index.md) + +## Stats +- Total sources: 4 +- Last updated: 2026-01-03 diff --git a/tests/fixtures/defects/raw-source-unresolved/raw/articles/2026-01-01-sample-article.md b/tests/fixtures/defects/raw-source-unresolved/raw/articles/2026-01-01-sample-article.md new file mode 100644 index 0000000..e07a29a --- /dev/null +++ b/tests/fixtures/defects/raw-source-unresolved/raw/articles/2026-01-01-sample-article.md @@ -0,0 +1,14 @@ +--- +title: "Sample Article Source" +source: https://example.com/testing-patterns +type: articles +ingested: 2026-01-01 +tags: [testing, patterns] +summary: "An article about testing patterns for LLM-powered tools." +--- + +# Sample Article Source + +Testing LLM-powered tools requires a layered approach: structural validation for file integrity, behavioral evals for instruction compliance, and integration scenarios for full workflows. + +The three-layer model is the consensus across Anthropic, OpenAI, and GitLab. diff --git a/tests/fixtures/defects/raw-source-unresolved/raw/articles/2026-01-02-second-article.md b/tests/fixtures/defects/raw-source-unresolved/raw/articles/2026-01-02-second-article.md new file mode 100644 index 0000000..292c747 --- /dev/null +++ b/tests/fixtures/defects/raw-source-unresolved/raw/articles/2026-01-02-second-article.md @@ -0,0 +1,14 @@ +--- +title: "Second Article Source" +source: https://example.com/eval-frameworks +type: articles +ingested: 2026-01-02 +tags: [testing, frameworks] +summary: "A survey of evaluation frameworks for testing AI agents." +--- + +# Second Article Source + +Promptfoo and DeepEval are the two leading open-source eval frameworks. Promptfoo is YAML-driven and language-agnostic. DeepEval is Python-native with richer agent metrics. + +Both support CI/CD integration via GitHub Actions. diff --git a/tests/fixtures/defects/raw-source-unresolved/raw/articles/2026-01-03-Title Cased Source.md b/tests/fixtures/defects/raw-source-unresolved/raw/articles/2026-01-03-Title Cased Source.md new file mode 100644 index 0000000..87d5cf7 --- /dev/null +++ b/tests/fixtures/defects/raw-source-unresolved/raw/articles/2026-01-03-Title Cased Source.md @@ -0,0 +1,14 @@ +--- +title: "Title Cased Source" +source: https://example.com/title-cased-source +type: articles +ingested: 2026-01-03 +tags: [testing, provenance] +summary: "A source with whitespace and title case in its filename for resolver regression coverage." +--- + +# Title Cased Source + +This source intentionally keeps whitespace and title case in the filename. It +proves source provenance checks preserve complete paths rather than splitting +on whitespace. diff --git a/tests/fixtures/defects/raw-source-unresolved/raw/articles/_index.md b/tests/fixtures/defects/raw-source-unresolved/raw/articles/_index.md new file mode 100644 index 0000000..a832a70 --- /dev/null +++ b/tests/fixtures/defects/raw-source-unresolved/raw/articles/_index.md @@ -0,0 +1,7 @@ +# Articles + +| Date | Title | Quality | Tags | +|------|-------|---------|------| +| 2026-01-01 | [Sample Article Source](2026-01-01-sample-article.md) | 4/5 | testing, patterns | +| 2026-01-02 | [Second Article Source](2026-01-02-second-article.md) | 4/5 | testing, frameworks | +| 2026-01-03 | [Title Cased Source](<2026-01-03-Title Cased Source.md>) | 4/5 | testing, provenance | diff --git a/tests/fixtures/defects/raw-source-unresolved/raw/data/_index.md b/tests/fixtures/defects/raw-source-unresolved/raw/data/_index.md new file mode 100644 index 0000000..db9230f --- /dev/null +++ b/tests/fixtures/defects/raw-source-unresolved/raw/data/_index.md @@ -0,0 +1,4 @@ +# Data + +| Date | Title | Quality | Tags | +|------|-------|---------|------| diff --git a/tests/fixtures/defects/raw-source-unresolved/raw/notes/_index.md b/tests/fixtures/defects/raw-source-unresolved/raw/notes/_index.md new file mode 100644 index 0000000..d0d1e97 --- /dev/null +++ b/tests/fixtures/defects/raw-source-unresolved/raw/notes/_index.md @@ -0,0 +1,4 @@ +# Notes + +| Date | Title | Quality | Tags | +|------|-------|---------|------| diff --git a/tests/fixtures/defects/raw-source-unresolved/raw/papers/2026-01-01-sample-paper.md b/tests/fixtures/defects/raw-source-unresolved/raw/papers/2026-01-01-sample-paper.md new file mode 100644 index 0000000..511c31c --- /dev/null +++ b/tests/fixtures/defects/raw-source-unresolved/raw/papers/2026-01-01-sample-paper.md @@ -0,0 +1,14 @@ +--- +title: "Sample Paper Source" +source: ../../../research/nonexistent-original.pdf +type: papers +ingested: 2026-01-01 +tags: [evals, methodology] +summary: "A paper on evaluation methodology for AI agents, covering pass@k reliability metrics." +--- + +# Sample Paper Source + +Pass@k measures capability (at least 1 success in k runs). Pass^k measures reliability (all k runs succeed). A system can score 100% pass@10 and 0% pass^10 simultaneously. + +For production systems, pass^k is the standard. diff --git a/tests/fixtures/defects/raw-source-unresolved/raw/papers/_index.md b/tests/fixtures/defects/raw-source-unresolved/raw/papers/_index.md new file mode 100644 index 0000000..b712b72 --- /dev/null +++ b/tests/fixtures/defects/raw-source-unresolved/raw/papers/_index.md @@ -0,0 +1,5 @@ +# Papers + +| Date | Title | Quality | Tags | +|------|-------|---------|------| +| 2026-01-01 | [Sample Paper Source](2026-01-01-sample-paper.md) | 5/5 | evals, methodology | diff --git a/tests/fixtures/defects/raw-source-unresolved/raw/repos/_index.md b/tests/fixtures/defects/raw-source-unresolved/raw/repos/_index.md new file mode 100644 index 0000000..c0fde83 --- /dev/null +++ b/tests/fixtures/defects/raw-source-unresolved/raw/repos/_index.md @@ -0,0 +1,4 @@ +# Repos + +| Date | Title | Quality | Tags | +|------|-------|---------|------| diff --git a/tests/fixtures/defects/raw-source-unresolved/schema.md b/tests/fixtures/defects/raw-source-unresolved/schema.md new file mode 100644 index 0000000..b34bcb4 --- /dev/null +++ b/tests/fixtures/defects/raw-source-unresolved/schema.md @@ -0,0 +1,55 @@ +--- +title: "Test Wiki Topic Guide" +schema_state: advisory +created: 2026-01-10 +updated: 2026-01-10 +summary: "Human-owned topic guide for local vocabulary, relationships, source boundaries, and conventions." +--- + +# Test Wiki Topic Guide + +> This is not a database schema and it does not make existing wiki content invalid. +> It is a human-owned guide for local vocabulary, relationships, source boundaries, and conventions. +> The librarian may propose improvements, but agents should not rewrite this file without explicit approval. + +## State + +- `schema_state`: `advisory` +- `advisory` means suggestions only. +- Keep this advisory until the librarian's topic-guide suggestions are consistently low-noise. +- `strict` is an advanced, explicit opt-in; it still never permits automatic content rewrites. + +## Entity Types + +| Type | Meaning | +|------|---------| +| `concept` | Bounded idea or mechanism compiled under `wiki/concepts/`. | +| `topic` | Broad theme or playbook compiled under `wiki/topics/`. | +| `reference` | Curated list, map, standard, or lookup page under `wiki/references/`. | +| `source` | Raw evidence under `raw/`; factual claims should trace back here. | +| `artifact` | Generated output, project file, inventory record, or dataset manifest. | + +## Relationship Verbs + +- `cites`: article or output cites a raw source. +- `supports`: source or article supports a claim, plan, or decision. +- `contradicts`: source or article conflicts with another claim. +- `supersedes`: newer article/output replaces an older one. +- `depends-on`: artifact or workflow relies on another artifact. +- `implements`: code, output, or project implements a plan. +- `relates-to`: weak relationship used when a stronger verb is not yet justified. + +## Source Conventions + +- Keep raw sources immutable under `raw/`. +- Compile durable synthesis under `wiki/` with explicit `sources:` frontmatter. +- Keep generated deliverables under `output/`. +- Use inventory for durable tracking state, not factual evidence. +- Use dataset manifests for large, mutable, binary, remote, or query-oriented data. + +## Adoption Notes + +- Existing articles do not need immediate rewrites to adopt this guide. +- Librarian topic-guide scans should propose changes in `output/schema-proposal-*.md`. +- Promote only the small conventions that fit this topic; delete unused starter rows. +- Prefer staying advisory unless the human explicitly wants stricter convention warnings. diff --git a/tests/fixtures/defects/raw-source-unresolved/wiki/_index.md b/tests/fixtures/defects/raw-source-unresolved/wiki/_index.md new file mode 100644 index 0000000..0e65361 --- /dev/null +++ b/tests/fixtures/defects/raw-source-unresolved/wiki/_index.md @@ -0,0 +1,13 @@ +# Wiki Articles + +## Concepts +See [concepts/_index.md](concepts/_index.md) + +## Topics +See [topics/_index.md](topics/_index.md) + +## References +See [references/_index.md](references/_index.md) + +## Theses +See [theses/_index.md](theses/_index.md) diff --git a/tests/fixtures/defects/raw-source-unresolved/wiki/concepts/_index.md b/tests/fixtures/defects/raw-source-unresolved/wiki/concepts/_index.md new file mode 100644 index 0000000..763c5f7 --- /dev/null +++ b/tests/fixtures/defects/raw-source-unresolved/wiki/concepts/_index.md @@ -0,0 +1,5 @@ +# Concepts + +| Article | Summary | Tags | +|---------|---------|------| +| [Sample Concept](sample-concept.md) | Testing patterns for LLM tools — three-layer model | testing, patterns, evals | diff --git a/tests/fixtures/defects/raw-source-unresolved/wiki/concepts/sample-concept.md b/tests/fixtures/defects/raw-source-unresolved/wiki/concepts/sample-concept.md new file mode 100644 index 0000000..71f653e --- /dev/null +++ b/tests/fixtures/defects/raw-source-unresolved/wiki/concepts/sample-concept.md @@ -0,0 +1,31 @@ +--- +title: "Sample Concept" +category: concept +sources: + - raw/articles/2026-01-01-sample-article.md + - raw/papers/2026-01-01-sample-paper.md + - "raw/articles/2026-01-03-Title Cased Source.md" +created: 2026-01-01 +updated: 2026-01-03 +tags: [testing, patterns, evals] +confidence: high +volatility: warm +verified: 2026-01-03 +summary: "Testing patterns for LLM tools — three-layer model with pass@k reliability metrics." +--- + +# Sample Concept + +Testing LLM-powered tools requires a layered approach. The three-layer model splits tests into structural validation (deterministic, no LLM), behavioral evals (semantic, LLM-graded), and integration scenarios (end-to-end workflows). For framework comparisons, see [[sample-reference|Sample Reference]] ([Sample Reference](../references/sample-reference.md)). + +Pass@k measures capability while pass^k measures reliability. Both metrics are needed. + +## See Also + +- [[sample-reference|Sample Reference]] ([Sample Reference](../references/sample-reference.md)) — tools and frameworks for implementing tests + +## Sources + +- [Sample Article Source](../../raw/articles/2026-01-01-sample-article.md) — three-layer testing model +- [Sample Paper Source](../../raw/papers/2026-01-01-sample-paper.md) — pass@k reliability metrics +- [Title Cased Source](<../../raw/articles/2026-01-03-Title Cased Source.md>) — whitespace filename source-resolution regression diff --git a/tests/fixtures/defects/raw-source-unresolved/wiki/references/_index.md b/tests/fixtures/defects/raw-source-unresolved/wiki/references/_index.md new file mode 100644 index 0000000..a3c5b57 --- /dev/null +++ b/tests/fixtures/defects/raw-source-unresolved/wiki/references/_index.md @@ -0,0 +1,5 @@ +# References + +| Article | Summary | Tags | +|---------|---------|------| +| [Sample Reference](sample-reference.md) | Eval frameworks — Promptfoo, DeepEval, comparison | testing, frameworks, tools | diff --git a/tests/fixtures/defects/raw-source-unresolved/wiki/references/sample-reference.md b/tests/fixtures/defects/raw-source-unresolved/wiki/references/sample-reference.md new file mode 100644 index 0000000..c961d2d --- /dev/null +++ b/tests/fixtures/defects/raw-source-unresolved/wiki/references/sample-reference.md @@ -0,0 +1,31 @@ +--- +title: "Sample Reference" +category: reference +sources: + - raw/articles/2026-01-02-second-article.md +created: 2026-01-02 +updated: 2026-01-02 +tags: [testing, frameworks, tools] +confidence: high +volatility: warm +verified: 2026-01-02 +summary: "Eval frameworks — Promptfoo (YAML, language-agnostic) vs DeepEval (Python, richer metrics)." +--- + +# Sample Reference + +## Promptfoo + +Open-source, YAML-driven, CLI-first eval framework with native Claude Agent SDK support. + +## DeepEval + +Python-native, pytest-based. Eight agent-specific metrics including Task Completion and Tool Correctness. + +## See Also + +- [[sample-concept|Sample Concept]] ([Sample Concept](../concepts/sample-concept.md)) — the testing methodology these tools implement + +## Sources + +- [Second Article Source](../../raw/articles/2026-01-02-second-article.md) — framework comparison diff --git a/tests/fixtures/defects/raw-source-unresolved/wiki/theses/_index.md b/tests/fixtures/defects/raw-source-unresolved/wiki/theses/_index.md new file mode 100644 index 0000000..601ebb2 --- /dev/null +++ b/tests/fixtures/defects/raw-source-unresolved/wiki/theses/_index.md @@ -0,0 +1,4 @@ +# Theses + +| Thesis | Status | Verdict | Tags | +|--------|--------|---------|------| diff --git a/tests/fixtures/defects/raw-source-unresolved/wiki/topics/_index.md b/tests/fixtures/defects/raw-source-unresolved/wiki/topics/_index.md new file mode 100644 index 0000000..23e91d2 --- /dev/null +++ b/tests/fixtures/defects/raw-source-unresolved/wiki/topics/_index.md @@ -0,0 +1,4 @@ +# Topics + +| Article | Summary | Tags | +|---------|---------|------| diff --git a/tests/generate-defect-fixtures.sh b/tests/generate-defect-fixtures.sh index 90c2577..a08cdf2 100755 --- a/tests/generate-defect-fixtures.sh +++ b/tests/generate-defect-fixtures.sh @@ -75,6 +75,13 @@ rm -f "$DEFECTS/dangling-source-ref/wiki/concepts/sample-concept.md.bak" \ "$DEFECTS/dangling-source-ref/inventory/items/trx4m-ring-and-pinion.md.bak" echo " Created: dangling-source-ref (C4b)" +# C4b: raw-source-unresolved — explicit local source: path does not resolve +copy_golden "raw-source-unresolved" +sed -i.bak 's|^source: https://example.com/eval-methodology$|source: ../../../research/nonexistent-original.pdf|' \ + "$DEFECTS/raw-source-unresolved/raw/papers/2026-01-01-sample-paper.md" +rm -f "$DEFECTS/raw-source-unresolved/raw/papers/2026-01-01-sample-paper.md.bak" +echo " Created: raw-source-unresolved (C4b)" + # C4b: retracted-marker — left in body copy_golden "retracted-marker" echo '' \ diff --git a/tests/test-local-cli-lint.sh b/tests/test-local-cli-lint.sh index 441e51c..ddbf647 100755 --- a/tests/test-local-cli-lint.sh +++ b/tests/test-local-cli-lint.sh @@ -284,6 +284,34 @@ expect_failure_contains \ "Invalid type" \ "$CLI" lint "$SCRIPT_DIR/fixtures/defects/bad-frontmatter" +expect_failure_contains \ + "explicit unresolved raw source path fails local lint" \ + "Raw source reference does not resolve" \ + "$CLI" lint "$SCRIPT_DIR/fixtures/defects/raw-source-unresolved" + +raw_source_compat="$tmpdir/raw-source-compat" +mkdir "$raw_source_compat" +cp -R "$GOLDEN/." "$raw_source_compat/" +mkdir "$tmpdir/local-source-repository" +mkdir "$tmpdir/local source files" +printf '# Original source\n' > "$tmpdir/local source files/original.md" +sed -i.bak 's|^source: https://example.com/testing-patterns$|source: session|' \ + "$raw_source_compat/raw/articles/2026-01-01-sample-article.md" +sed -i.bak 's|^source: https://example.com/eval-frameworks$|source: s3://example-bucket/source.md|' \ + "$raw_source_compat/raw/articles/2026-01-02-second-article.md" +sed -i.bak "s|^source: https://example.com/title-cased-source$|source: ../../../local source files/original.md|" \ + "$raw_source_compat/raw/articles/2026-01-03-Title Cased Source.md" +sed -i.bak "s|^source: https://example.com/eval-methodology$|source: file://$tmpdir/local-source-repository|" \ + "$raw_source_compat/raw/papers/2026-01-01-sample-paper.md" +rm -f "$raw_source_compat"/raw/articles/*.bak "$raw_source_compat"/raw/papers/*.bak +cat >> "$raw_source_compat/raw/articles/2026-01-01-sample-article.md" <<'EOF' + +Upstream navigation remains source content: [contributor guide](docs/CONTRIBUTING.md) +EOF +expect_success \ + "raw provenance sentinels, URIs, valid paths, directories, and upstream links stay compatible" \ + "$CLI" lint "$raw_source_compat" + ideas_wiki="$tmpdir/ideas-wiki" mkdir "$ideas_wiki" cp -R "$GOLDEN/." "$ideas_wiki/" diff --git a/tests/test-structure.sh b/tests/test-structure.sh index 9f5227a..bb090e3 100755 --- a/tests/test-structure.sh +++ b/tests/test-structure.sh @@ -452,6 +452,12 @@ if [ -d "$DEFECTS" ]; then || log_fail "dangling-source-ref: no dangling ref" "fixture broken" } + [ -d "$DEFECTS/raw-source-unresolved" ] && { + grep -q "source: ../../../research/nonexistent-original.pdf" "$DEFECTS/raw-source-unresolved/raw/papers/2026-01-01-sample-paper.md" 2>/dev/null \ + && log_pass "raw-source-unresolved: C4b defect present" \ + || log_fail "raw-source-unresolved: no unresolved raw source path" "fixture broken" + } + [ -d "$DEFECTS/retracted-marker" ] && { grep -q "RETRACTED-SOURCE" "$DEFECTS/retracted-marker/wiki/concepts/sample-concept.md" 2>/dev/null \ && log_pass "retracted-marker: C4b defect present" \