Claude Skill

validate-md-ref

检查 Markdown 文档中的引用是否可定位、URL 或锚点是否可访问,并整理供后续判断引用真实性与适切性的结构化证据。当用户要求核查引用、检查文档链接,或确认引用是否支持正文论断时使用。

LLM Mart · 0 points · 17 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download huangwb8-skills-skills_beta_validate-md-ref-0b4095f.zip · 33 KB
Part of huangwb8/skills — 22 skills

Install

skills CLI npx skills add https://github.com/huangwb8/skills/tree/main/skills/beta/validate-md-ref
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install huangwb8-skills@llmmart
Git git clone https://github.com/huangwb8/skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole huangwb8/skills collection as a plugin from our marketplace. Git is the plain clone.

README

validate-md-ref

当前版本:0.13.4。这个 skill 将 Markdown 作为输入适配层,提取引用并采集 URL/锚点事实,再交由目录化 verifier 协议判断引用完整性与引用真实性。每次运行都会强制经过使用 canonical State ID 的 kernel 状态机并执行 canonical Verifier;它不把链接可达性冒充语义结论,也不自动修改原文档。

它适合跨格式引用核验;Markdown 只是当前可用的输入适配器。语义引擎缺口会明确标为 unchecked 或 manual_review。调用脚本时请使用 Bensz 托管运行时的 Python;runtime.kernel.version 是最低兼容版本,新版 Kernel 还会核验声明的 capabilities。

执行细节按需阅读:

用法

最推荐用法

请使用 validate-md-ref skill 验证这个 Markdown 文档中的 URL 引用是否可访问。
输入:`/path/to/file.md`
输出:JSON 格式的结构化验证结果,包含有效、无效和跳过的链接统计

进阶用法

请使用 validate-md-ref skill 检查这个 Markdown 文档的 URL 引用。
输入:`/path/to/file.md`
输出:验证结果
另外,还有下列参数约束:
- 使用自定义配置文件:`custom-config.yaml`
- 结果里按引用类型分类
- 保留失败原因

能做什么

  • 提取 Markdown 里的多种 URL 引用形式。
  • 对每个 URL 做可达性检查和安全校验。
  • 输出结构化结果,方便后续人工或 AI 进一步处理。
  • 适合文档质检、交付前巡检、链接清单复核。
  • 不负责自动改写正文内容,也不直接替你决定如何处理无效链接。

使用示例

示例 1:检查单个 Markdown

请使用 validate-md-ref skill 验证这个 Markdown 文档中的 URL 引用。
输入:`README.md`
输出:JSON 结构化验证结果

示例 2:按自定义规则检查

请使用 validate-md-ref skill 检查这个 Markdown 文件。
输入:`docs/review.md`
输出:验证结果
另外,还有下列参数约束:
- 使用自定义配置文件:`validate-md-ref/config.yaml`

示例 3:为交付做最后巡检

请使用 validate-md-ref skill 检查这份 Markdown 交付文档的引用质量。
输入:`deliverable.md`
输出:有效、无效和跳过链接的结构化结果

输出

  • 核心输出是结构化验证结果,可继续转成 Markdown 报告。
  • 常见结果字段包括:
    • summary.total
    • summary.valid
    • summary.invalid
    • summary.skipped
    • references[*].validation
  • 当前脚本直接把 JSON 结果输出到标准输出,不会自动生成独立 Markdown 报告文件。
  • 使用状态机执行器时产生 log/meta-state.json 状态快照;传入 --events 时产生 log/events.ndjson Verifier 事件账本。直接脚本调用不会隐式创建任务工作区,若任务要求审计必须显式提供这些入口。
  • verification.results 保存原子规则结果与证据引用;本 Skill 的 verification.gate 用 allow 或 reject 表达链接完整性结果。格式无关的语义 Pack 才用 manual_review 表达验证缺口。
  • verification.metrics 保存 Kernel 计算的 Verifier 覆盖率、未知/不确定比例、Gate 放行率、assurance tier 与耗时指标。

配置

  • 配置文件:validate-md-ref/config.yaml
  • 默认超时:10 秒
  • 重定向由 kernel 以固定上限逐跳处理,并在每一跳发起请求前重新执行安全检查。
  • 支持域名白名单和黑名单。
  • 关键配置节:
    • validation
    • domain_whitelist
    • domain_blacklist
    • runtime:声明 references/states 状态包及链接/语义 Verifier 版本;状态包使用 references/states/index.json 的 bensz-pack-index-v1 清单

Verifier 契约由 bensz-skill-kernel 内置 registry 统一维护;本 Skill 只声明调用方式和验证边界。

bensz.evidence.citation-truth-fit 是唯一的引用 Verifier,不受文档类型限制;本 Skill 负责将 Markdown 转成它所需的标准证据。旧 ID citation.truth-and-fit 仅作为兼容 alias。详见 引用真实性与适切性契约。

直接调用 bsk 时,配置文件不会自动加载。请通过格式适配器提交结构化证据,再调用 bensz.evidence.citation-truth-fit@1.0.0;不能把文档路径直接当作通用语义输入。

备选用法(脚本/硬编码)

如果你已经知道要检查哪个 Markdown 文件,直接调用脚本就可以得到结构化结果。

使用默认配置

python3 validate-md-ref/scripts/validate_links.py README.md

指定自定义配置

python3 validate-md-ref/scripts/validate_links.py \
  docs/review.md \
  validate-md-ref/config.yaml

调用 kernel 内置 Verifier

这个 Skill 通过 runtime 声明链接完整性为 required、引用语义为 advisory,并保留两个 Verifier 的独立结果:

bsk verifier list --tag common
bsk verifier describe bensz.document.markdown-link-integrity --version 1.0.0
bsk verifier describe bensz.evidence.citation-truth-fit --version 1.0.0

所需 verifier:bensz.evidence.citation-truth-fit,版本 1.0.0。它带有 common、citation、semantic、evidence 标签;格式适配器负责提供标准证据,Verifier 输出统一的 verification.results 与 verification.gate。

常见问题

Q:它会自动把无效链接从 Markdown 中删掉吗?

A:不会。它负责“检查并报告”,后续是否删除、替换或标注,需要你或 AI 继续判断。

Q:为什么结果里会出现 skipped?

A:因为脚本会根据白名单和黑名单跳过某些域名验证,例如本地地址、内网地址,或不在白名单范围内的域名。

Q:为什么有些本地或内网地址会被跳过?

A:默认黑名单会排除 localhost、127.0.0.1、*.local、*.internal 等域名,避免把本地开发环境误当成公开可访问链接。

Q:它能检查 Markdown 以外的格式吗?

A:Markdown 只是当前 Skill 的输入适配器。Verifier bensz.evidence.citation-truth-fit 本身不限制 Markdown,LaTeX、Word 或其它格式适配器都可以提交同样的标准证据。

Skill manifest

validate-md-ref

目标

检查 Markdown 文档中的引用是否可定位、URL 或锚点是否可访问,并整理供后续判断引用真实性与适切性的结构化证据。当用户要求核查引用、检查文档链接,或确认引用是否支持正文论断时使用。

流程

输入

范围与边界

  • 输入:一个 Markdown 文件,可选一个 YAML 配置文件。
  • 检查:Markdown 行内链接、HTML <a href> 链接、当前文档内的 #anchor,以及外部 HTTP(S) 链接的可达性。
  • 输出:结构化 JSON,逐条保留引用位置、验证状态和失败或跳过原因。
  • 不做:不修改原文;URL 可达性不等于来源支持论断;不代替用户决定修复方式。
  • 运行时能力:使用 bensz.document.markdown-link-integrity 检查链接事实,并保留 bensz.evidence.citation-truth-fit 的语义复核状态;两者版本独立记录,旧 ID 仅作兼容 alias。

执行步骤

强制运行门禁

每次执行必须经过 Bensz Skill Kernel 状态机并调用指定版本的链接完整性 Verifier。任一环节不可用或失败,任务即未完成并须说明原因;不得降级为普通脚本或手工检查。

状态机使用 bensz.workspace.ready 作为系统入口,并依次进入 bensz.validate-md-ref.input-ready、bensz.validate-md-ref.checking 和 bensz.validate-md-ref.reported;旧 State ID 仅作兼容 alias。

AI 应使用本 Skill 的执行器或封装入口,不得手工模拟状态转移、拼接事件账本或复制 Verifier 规则。命令、上下文和事件契约见:

流程

  1. 确认 Markdown 存在并只读处理;需限制网络请求时加载默认或指定 YAML。
  2. 提取并分类检查:站内锚点在当前文档定位;HTTP(S) 按安全策略验证可达性;白/黑名单命中则跳过。
  3. 保留链接 Verifier 标准结果;真实性或适切性仅保留语义 Verifier 的 unchecked/manual_review,无证据不得判通过。
  4. 汇总总数、有效、无效、跳过项,逐条披露位置、状态、失败原因和语义边界。

网络 DNS、连接失败和超时属于 unresolved/timed_out 观测不确定性,不得当作确定性链接失效;只有 HTTP 明确错误、本地 anchor 缺失或越界文件才计入 invalid。

命令行入口以 kernel bensz.document.markdown-link-integrity Pack 返回的 facts.summary 与 facts.references 为唯一链接事实来源;不要将旧兼容函数的本地 探测结果与 Verifier 结果合并或互相覆盖。

工具

  • scripts/validate_links.py:读取 Markdown 及可选 YAML,输出结构化结果。
  • config.yaml:提供默认超时、域名白名单和黑名单,并在 runtime 节声明状态包与 Verifier 选择。

从 Skill 目录调用脚本;工作目录不同则使用绝对路径或先切换目录。运行入口负责状态机和 Verifier 门禁,不得绕过门禁解释脚本结果。

常用业务调用形式:

python3 scripts/validate_links.py DOCUMENT.md
python3 scripts/validate_links.py DOCUMENT.md CONFIG.yaml

输出字段和配置字段的完整说明见 references/formats.md 与 references/tools.md。

输出

输出结构化引用检查结果、可定位证据和报告:记录每条引用的来源、URL/锚点状态、错误或不确定原因,以及供后续真实性/适切性判断使用的 Evidence 快照;不修改源 Markdown。

输出管理

文件边界

需落盘时,将本 Skill 的输入、临时结果和日志写入当前会话声明的 ./.bensz-api/task-{yyyymmdd-hhmm}-{简短描述}/validate-md-ref/{input,output,log}/;多 Skill 共享材料放任务根目录 shared/。正式交付物、用户指定文件和源 Markdown 留在项目约定位置。不得写入密钥、令牌、Cookie、私有指令、隐私或不必要的大体积原始数据;纯文本答复无需建目录。

校验

校验输入路径位于允许的 base_dir、拒绝越界/symlink 逃逸和敏感路径,按配置检查 URL/锚点、重定向、域名白黑名单和超时;required 链接完整性通过后才可放行,advisory 真实性判断仅作提示并保留人工复核。

失败与恢复

文件不可读、路径越界、URL/锚点检查超时或外部站点不可用时,保留已收集的 Evidence、错误分类和日志,按 required/advisory 规则阻断或标记 uncertain/unchecked;修复输入或网络后可在同一任务工作区重试,不把缺失证据视为通过。

控制

运行时由 bensz-skill-kernel 按 config.yaml.runtime 管理 State、Verifier 与 Gate;链接完整性为 required,语义真实性为 advisory,失败或不确定时保留证据并转人工复核。

约束

公共硬约束

本块由 docs/templates/skill-common-constraints.md 统一维护;每个 SKILL.md 的 ## 约束 必须逐字同步本块,不得在副本中改写公共规则。

  • 任务需要落盘时,使用唯一的 ./.bensz-api/task-{yyyymmdd-hhmm}-{简短描述}/ 根目录;共享材料放入 shared/,Skill 专属材料放入该 Skill 的 input/、output/、log/。
  • 正式交付物、源代码和正式计划按项目约定保存,不写入任务工作区;未经授权不覆盖、删除、迁移或远程写入。
  • 项目维护变更检查 BAC 可用性并记录需求、AI 产出、工具结果、文件改动和验证摘要;BAC 只做过程审计,不替代署名、责任或合规判断。
  • 不记录 API Key、访问令牌、密码、Cookie、环境/凭据文件、私有 Prompt、身份信息、本地用户名、主机名或不必要的大体积原始数据。
  • 文件路径必须规范化并限制在授权项目范围内;外部 URL、子进程和网络访问遵循最小权限,防止路径遍历、SSRF 和命令注入。
  • Skill 版本唯一记录在自身 config.yaml:skill_info.version;公开 API、协议、目录或配置变更同步文档与 CHANGELOG.md。
  • bensz-collect-bugs 是一个 Agent Skill;仅将 Bensz Agent Skill 或 Bensz 基础设施本身的设计缺陷交给它。先脱敏写入 ~/.bensz-skills/bugs/,当前任务不中断,只有用户明确要求才公开上报,禁止直接修改用户已安装的 Skill 源码。

Skill 专属约束

疑似 Skill 设计问题

  • 适用范围:仅记录流程漏判、输入约定不完整、环境假设错误等 Skill 设计缺陷;用户数据错误、第三方波动和偶发模型输出除外。
  • 隐私保护:不得记录密钥、密码、身份信息、邮箱、私密路径、用户名、主机名或工作目录;公开前须脱敏。
  • 本地优先:先写入 ~/.bensz-skills/bugs/,不打断任务;仅用户明确要求时用本机 gh api 上报。
  • 禁止就地修 bug:不要直接修改用户本地已安装 Skill 源码;先记录,再继续任务。
Files (skills)
  • qa
    • test_anchor_and_get_fallback.py 5.5 KB
      from __future__ import annotations
      
      import importlib.util
      import json
      import sys
      import tempfile
      import unittest
      from contextlib import redirect_stdout
      from io import StringIO
      from pathlib import Path
      from unittest.mock import patch
      
      
      SCRIPT = Path(__file__).resolve().parents[1] / 'scripts' / 'validate_links.py'
      SPEC = importlib.util.spec_from_file_location('validate_links', SCRIPT)
      assert SPEC and SPEC.loader
      MODULE = importlib.util.module_from_spec(SPEC)
      sys.modules[SPEC.name] = MODULE
      SPEC.loader.exec_module(MODULE)
      
      
      class AnchorAndGetFallbackTests(unittest.TestCase):
          def test_relative_document_link_is_checked_as_local_file(self) -> None:
              with tempfile.TemporaryDirectory() as directory:
                  root = Path(directory)
                  (root / "docs").mkdir()
                  (root / "docs" / "guide.md").write_text("# Install\n", encoding="utf-8")
                  refs = MODULE.extract_references("[guide](docs/guide.md#install)")
                  results = MODULE.validate_references(refs, {}, "", root)
                  self.assertTrue(results[0]["validation"]["valid"])
          def test_local_anchor_is_validated_against_heading_and_html_id(self) -> None:
              content = '# 使用方法\n\n<a id="custom-anchor"></a>\n[标题](#使用方法) [显式](#custom-anchor) [缺失](#missing)'
              refs = MODULE.extract_references(content)
              results = MODULE.validate_references(refs, {}, content)
              self.assertEqual([item['validation']['valid'] for item in results], [True, True, False])
              self.assertTrue(all(item['validation'].get('local_anchor') for item in results))
      
          def test_head_405_falls_back_to_limited_get(self) -> None:
              with patch.object(MODULE.subprocess, 'check_output', side_effect=['405\nhttps://example.test', '200\nhttps://example.test']) as call:
                  result = MODULE.validate_url('https://example.test')
              self.assertTrue(result['valid'])
              self.assertEqual(result['status_code'], 200)
              self.assertEqual(call.call_count, 2)
              self.assertIn('-I', call.call_args_list[0].args[0])
              self.assertIn('--range', call.call_args_list[1].args[0])
      
          def test_runtime_events_use_kernel_command(self) -> None:
              from bensz_skill_kernel import EventLog
      
              with tempfile.TemporaryDirectory() as directory:
                  events = Path(directory) / 'events.ndjson'
                  result = MODULE.record_runtime_events(
                      str(events),
                      [{'verifier_id': 'bensz.evidence.citation-truth-fit', 'verifier_version': '1.0.0', 'verdict': 'unchecked', 'execution_status': 'unchecked', 'evidence_refs': ['subject_context']}],
                      {'decision': 'manual_review', 'reason': 'verification gap or semantic uncertainty'},
                      'run-test',
                  )
                  self.assertTrue(result['recorded'])
                  projection = EventLog(events).projection()
                  self.assertEqual(projection['verifications'][0]['request_id'], 'run-test')
                  self.assertEqual(projection['gate_decisions'][0]['decision'], 'wait')
                  self.assertEqual(projection['gate_decisions'][0]['computed_by'], 'kernel')
      
          def test_skill_state_declaration_uses_indexed_state_pack(self) -> None:
              from bensz_skill_kernel import SkillStateDeclaration
      
              skill_root = SCRIPT.parents[1]
              declaration = SkillStateDeclaration.from_skill_root(skill_root)
              self.assertEqual(declaration.source.name, 'config.yaml')
              self.assertEqual(
                  {item.id for item in declaration.registry().definitions(kind='skill')},
                  {
                      'bensz.validate-md-ref.input-ready',
                      'bensz.validate-md-ref.checking',
                      'bensz.validate-md-ref.reported',
                  },
              )
              self.assertEqual(
                  declaration.registry().resolve('validate-md-ref.input-ready').id,
                  'bensz.validate-md-ref.input-ready',
              )
      
          def test_cli_uses_kernel_facts_instead_of_legacy_network_adapter(self) -> None:
              with tempfile.TemporaryDirectory() as directory:
                  document = Path(directory) / 'README.md'
                  document.write_text('# Guide\n\n[local](#guide)\n', encoding='utf-8')
                  output = StringIO()
                  with patch.object(MODULE, 'validate_references', side_effect=AssertionError('legacy adapter called')):
                      with redirect_stdout(output):
                          self.assertEqual(MODULE.main([str(document)]), 0)
                  payload = json.loads(output.getvalue())
                  self.assertEqual(payload['summary']['total'], 1)
                  self.assertTrue(payload['references'][0]['validation']['valid'])
                  self.assertEqual(payload['verification']['results'][0]['facts']['summary'], payload['summary'])
      
          def test_generate_summary_keeps_network_unknown_separate_from_invalid(self) -> None:
              results = [
                  {'validation': {'valid': True, 'validation_status': 'valid'}},
                  {'validation': {'valid': False, 'validation_status': 'unresolved'}},
                  {'validation': {'valid': False, 'validation_status': 'timed_out'}},
                  {'validation': {'valid': False, 'validation_status': 'invalid'}},
                  {'validation': {'valid': False, 'validation_status': 'skipped', 'skipped': True}},
              ]
      
              summary = MODULE.generate_summary(results)
      
              self.assertEqual(summary['valid'], 1)
              self.assertEqual(summary['invalid'], 1)
              self.assertEqual(summary['unresolved'], 2)
              self.assertEqual(summary['timed_out'], 1)
              self.assertEqual(summary['skipped'], 1)
      
      
      if __name__ == '__main__':
          unittest.main()
      
  • references
    • states
      • checking
        • STATE.md 698 B
          ---
          id: bensz.validate-md-ref.checking
          version: 1.0.0
          kind: skill
          description: Link facts are being collected and normalized by the selected verifier.
          aliases: validate-md-ref.checking
          entry_conditions: bensz.validate-md-ref.input-ready
          invariants: source-read-only, verifier-result-recorded
          transitions: bensz.validate-md-ref.reported
          ---
          
          # Checking
          
          Run the configured input adapter or `bensz.document.markdown-link-integrity`
          verifier. Preserve the normalized result and any Gate decision in the Skill log
          before proceeding. The Kernel enforces `verifier-result-recorded` when leaving
          this state: both `verification.result` and `verification.gate` events must be
          present in the task event log.
          
      • input-ready
        • scripts
          • check_input.py 985 B
            """Minimal state helper: verify that the selected Markdown input is readable."""
            
            from __future__ import annotations
            
            import json
            import sys
            from pathlib import Path
            
            
            def main() -> int:
                payload = json.load(sys.stdin)
                document = (payload.get("request", {}).get("context", {}) or {}).get("document")
                if not isinstance(document, str) or not document:
                    result = {"verdict": "fail", "summary": "context.document is required.", "facts": {}, "evidence_refs": []}
                else:
                    path = Path(document).expanduser()
                    valid = path.is_file() and path.suffix.lower() == ".md"
                    result = {"verdict": "pass" if valid else "fail", "summary": "Markdown input is readable." if valid else "context.document must name an existing Markdown file.", "facts": {"document": str(path.resolve())}, "evidence_refs": [str(path.resolve())] if valid else []}
                print(json.dumps(result, ensure_ascii=False))
                return 0
            
            
            if __name__ == "__main__":
                raise SystemExit(main())
            
        • STATE.md 574 B
          ---
          id: bensz.validate-md-ref.input-ready
          version: 1.0.0
          kind: skill
          description: A readable Markdown input was selected for this validation run.
          aliases: validate-md-ref.input-ready
          entry_conditions: bensz.workspace.ready
          invariants: input.read-only, no-secrets-in-workspace
          transitions: bensz.validate-md-ref.checking
          entrypoint: scripts/check_input.py
          ---
          
          # Input ready
          
          The Agent selected one existing Markdown input and will only read it. The helper
          requires `context.document` to point to a readable `.md` file; it does not copy
          the document into the task workspace.
          
      • reported
        • STATE.md 486 B
          ---
          id: bensz.validate-md-ref.reported
          version: 1.0.0
          kind: skill
          description: The validation result and its uncertainty were reported to the user.
          aliases: validate-md-ref.reported
          entry_conditions: bensz.validate-md-ref.checking
          invariants: result-standardized, uncertainty-disclosed
          transitions: bensz.workspace.closed
          ---
          
          # Reported
          
          Present the standardized result without treating a link-reachability fact as a
          semantic citation conclusion. Keep the original Markdown unchanged.
          
      • index.json 888 B
        {
          "protocol": "bensz-pack-index-v1",
          "package_kind": "state",
          "entries": [
            {
              "directory": "input-ready",
              "id": "bensz.validate-md-ref.input-ready",
              "version": "1.0.0",
              "kind": "skill",
              "classification": "domain",
              "aliases": ["validate-md-ref.input-ready"],
              "contract": "STATE.md",
              "entrypoint": "scripts/check_input.py"
            },
            {
              "directory": "checking",
              "id": "bensz.validate-md-ref.checking",
              "version": "1.0.0",
              "kind": "skill",
              "classification": "domain",
              "aliases": ["validate-md-ref.checking"],
              "contract": "STATE.md"
            },
            {
              "directory": "reported",
              "id": "bensz.validate-md-ref.reported",
              "version": "1.0.0",
              "kind": "skill",
              "classification": "domain",
              "aliases": ["validate-md-ref.reported"],
              "contract": "STATE.md"
            }
          ]
        }
        
    • citation-truth-and-fit.md 1.2 KB
      # 引用真实性与适切性契约
      
      `bensz.evidence.citation-truth-fit` 是格式无关的语义验证能力,不负责解析 Markdown、LaTeX、Word 或其它载体。各格式适配器先把引用归一化,再提交以下证据:
      
      - `subject_context`:被引用支持的目标论断、必要上下文和引用位置。
      - `source_metadata`:来源标题、作者、发布日期、标识符及可追溯位置。
      - `source_excerpt`:与目标论断直接相关的来源摘录;只有 URL 或书目信息不够。
      
      验证结果分别回答:
      
      - `evidence.identity`:来源身份与元数据能否被可靠确认。
      - `semantic.entailment`:来源证据是否支持目标论断,还是仅与主题相关。
      - `semantic.appropriateness`:引用位置、表述强度、时效性和来源类型是否恰当。
      
      缺少必需证据、来源无法获取、判断引擎不可用或证据存在冲突时,返回 `unchecked`、`uncertain` 或 `manual_review`,不得因为链接可访问就判定通过。
      
      当前 `validate-md-ref` 提供 Markdown 输入适配,并调用该通用 Pack。其它格式适配器也应提交同样的三类证据;kernel 只负责版本、证据引用、结果格式和 Gate 语义。
      
    • formats.md 1.4 KB
      # 输入与输出
      
      ## 输入
      
      目标是一个 Markdown 文件。工具会识别常见 Markdown 行内链接、HTML `<a href>` 链接和 `#anchor` 站内锚点。
      
      站内锚点在当前文档中检查;HTTP(S) 链接进行网络可达性检查。输入文件按只读处理。
      
      ## 输出
      
      结果为 JSON,常用字段:
      
      - `summary.total`、`summary.valid`、`summary.invalid`、`summary.unresolved`、`summary.skipped`
      - `references[*].validation.validation_status`:`valid`、`invalid`、`unresolved`、`timed_out` 或 `skipped`;网络不可观测不等同于链接失效。
      - `references[*].url`、`references[*].line_number`、`references[*].validation`
      - `verification`(直接命令也提供顶层 `results` 和 `gate`)
      - `verification.metrics`:Kernel 汇总的 Verifier 覆盖率、未知/不确定比例、Gate 放行率、assurance tier 与耗时指标。
      - `verification.requirements`:运行时声明的 required/advisory Verifier 及版本;Gate 仅对 required 的确定性失败拒绝。
      - `verification.runtime`:本次运行使用的 Kernel 名称/版本/来源与规范化 Pack 版本;同一元数据会随事件结果记录,便于审计环境漂移。
      
      可达性通过不代表网页内容支持正文论断;被安全策略跳过的地址也不等于链接失效。
      
      本 Skill 将 Markdown 事实适配到格式无关的 `bensz.evidence.citation-truth-fit` 契约;不要从 URL `valid: true` 推导语义结论。
      
    • state-machine.md 3.1 KB
      # 状态机契约
      
      状态机是本 Skill 的强制运行治理层,不替代 Markdown 检查流程。每次执行都必须通过 kernel 命令读取契约、执行合法转移并保存元状态快照;不得自行猜测状态含义、伪造快照或跳过状态机。
      
      ## 状态包
      
      Skill 根目录的 [`config.yaml`](../config.yaml) 的 `runtime` 节声明状态根和状态列表。旧版
      [`state-machine.json`](../state-machine.json) 仅作为兼容读取入口。`bensz.workspace.ready` 是强制初始状态,Skill 阶段按顺序为:
      同一节的 `kernel.name/version` 固定本 Skill 运行所需的 Kernel;事件记录发现版本不匹配时必须失败,不能静默使用 PATH 中的旧 `bsk`。
      
      `references/states/index.json` 是当前状态包的目录清单,采用 kernel 的
      `bensz-pack-index-v1` 协议;它集中声明 canonical ID、版本、alias 和入口脚本,
      `STATE.md` 保留状态契约正文。没有索引的旧目录仍由 kernel 兼容读取。
      
      `bensz.validate-md-ref.input-ready` → `bensz.validate-md-ref.checking` → `bensz.validate-md-ref.reported`
      
      各状态的入口条件、不变量和允许转移分别记录在 [`input-ready`](states/input-ready/STATE.md)、[`checking`](states/checking/STATE.md) 和 [`reported`](states/reported/STATE.md) 的 `STATE.md` 中。`input-ready` 只接受现有且可读的 Markdown 文件;`checking` 要求保留规范化验证结果;`reported` 要求向用户披露不确定性并保持原文档不变。
      
      其中 `checking` 的 `verifier-result-recorded` 是 Kernel 可执行的不变量:离开该状态前,任务级 `events.ndjson` 必须同时存在同一 `run_id/attempt_id` 下的 `verification.result` 和 `verification.gate`;运行身份必须成对提供,禁止只传其中一个,且 Gate 的 `result_refs` 还必须覆盖该运行的全部结果。该检查只证明结果已记录,不代表 Gate 一定放行。其余自然语言不变量仍由本 Skill 的 helper 或人工复核负责。
      
      ## 必经 Kernel 操作
      
      ```bash
      bsk workspace status TASK_ROOT
      bsk state list --skill-root SKILL_ROOT
      bsk state describe bensz.validate-md-ref.input-ready --skill-root SKILL_ROOT
      bsk state transition TASK_ROOT validate-md-ref bensz.validate-md-ref.input-ready \
        --skill-root SKILL_ROOT --context-json '{"document":"DOCUMENT.md"}'
      bsk state transition TASK_ROOT validate-md-ref bensz.validate-md-ref.checking \
        --skill-root SKILL_ROOT --context-json '{"document":"DOCUMENT.md"}'
      bsk state transition TASK_ROOT validate-md-ref bensz.validate-md-ref.reported \
        --skill-root SKILL_ROOT --context-json '{"document":"DOCUMENT.md"}'
      ```
      
      按 `input-ready`、`checking`、`reported` 顺序执行。每次转移成功后检查返回的标准 `bensz-meta-state-v1` JSON;状态快照写入任务工作区中 Skill 的 `log/meta-state.json`。任一转移失败即停止,不得把源 Markdown 复制进工作区,也不得在状态转移中写回原文档。
      成功转移还会追加带 `state_domain: skill`、前后 canonical 状态、运行身份和快照哈希的 `state.transition` 事件;可用 `bsk rebuild` 从事件账本重放 `skill_states`,快照仅作缓存。
      
    • tools.md 872 B
      # 工具包与命令
      
      ## 脚本
      
      ```bash
      python3 scripts/validate_links.py DOCUMENT.md [CONFIG.yaml]
      ```
      
      脚本负责加载配置并调用公共检查工具。默认配置来自 Skill 目录的 `config.yaml`。
      
      ## 直接命令
      
      Verifier 的命令、版本和结果边界集中记录在 [`verifiers.md`](verifiers.md)。直接调用时需要自行传入配置参数,例如 `--timeout 10`、重复的 `--blacklist DOMAIN` 或 `--whitelist DOMAIN`;需要审计时追加 `--events EVENTS.ndjson --run-id RUN_ID`。
      
      脚本写入事件时会将 `run_id` 同时传给 Kernel 事件 Envelope,便于按运行 ID 离线检索审计轨迹。
      
      ## 配置
      
      ```yaml
      validation:
        timeout: 10
      domain_whitelist: []
      domain_blacklist: []
      ```
      
      黑名单用于跳过不应访问的域名;白名单非空时只检查其中的域名。默认会避开本地、回环和内部地址。
      
    • verifiers.md 2.6 KB
      # Verifier 契约与边界
      
      本 Skill 只负责 Markdown 输入适配和链接事实采集;目录化 Verifier 负责按统一协议执行规则或语义判断。每次执行必须运行链接完整性 Verifier 并保留语义 Verifier 状态;不要在 Skill 侧复制 Verifier 注册表、规则或 Gate 逻辑。
      
      ## 可用 Verifier
      
      - `bensz.document.markdown-link-integrity@1.0.0`:检查 Markdown 链接、HTML `href` 和站内锚点的完整性与可达性;旧 ID `markdown.link-integrity`、`markdown.references` 作为 alias。
      - `bensz.evidence.citation-truth-fit@1.0.0`:格式无关的引用真实性与适切性 Verifier。它接收适配器提交的论断上下文、来源元数据和来源摘录,不直接解析 Markdown、LaTeX 或 Word;旧 ID `citation.truth-and-fit` 作为 alias。
      
      Markdown 解析、URL 请求和锚点检查产生的是输入事实,不能直接被解释为“来源支持正文论断”的语义结论。
      
      ## 强制调用
      
      查看目录与契约:
      
      ```bash
      bsk verifier describe bensz.document.markdown-link-integrity --version 1.0.0
      bsk verifier describe bensz.evidence.citation-truth-fit --version 1.0.0
      ```
      
      每次运行均须执行链接完整性 Verifier,并向任务事件账本写入标准化结果。推荐由脚本一次完成链接事实与语义状态:
      
      ```bash
      python3 scripts/validate_links.py DOCUMENT.md config.yaml \
        --events TASK_ROOT/log/events.ndjson --run-id RUN_ID
      ```
      
      脚本会用同一请求执行 `bensz.document.markdown-link-integrity@1.0.0` 与
      `bensz.evidence.citation-truth-fit@1.0.0`,并按 Kernel Gate 合并结果。直接调用
      `bsk verifier run` 时不会自动读取 Skill 的 `config.yaml`;超时、白名单和黑名单必须
      通过 CLI 参数显式传入。任何 Verifier、Gate 或事件写入失败都必须终止本次检查,不得降级为仅脚本或手工检查。
      
      ## 结果解释
      
      保留 Verifier 返回的 `verification.results` 与 `verification.gate`。链接事实通常用 `allow` 或 `reject` 表达;缺少语义证据、来源不可获取、判断引擎不可用或证据冲突时,应保留 `unchecked`、`uncertain` 或 `manual_review`,不得为了通过门禁而猜测。
      
      `bensz.evidence.citation-truth-fit` 的证据字段和语义判断详见 [`citation-truth-and-fit.md`](citation-truth-and-fit.md)。
      链接完整性 Verifier 将 HTTP 明确错误、本地 anchor 缺失和越界文件标为 `invalid/fail`;DNS、连接失败和超时标为 `unresolved`/`timed_out`,结果为 `unchecked` 或 `timed_out`,交由 Gate 进入人工复核,不把环境不可观测误判为链接失效。
      
  • scripts
    • validate_links.py 28.6 KB
      #!/usr/bin/env python3
      """
      Markdown 引用验证脚本
      
      功能:
      1. 提取 Markdown 文档中的所有 URL 引用
      2. 验证 URL 可达性
      3. 返回验证结果供 AI 进一步处理
      """
      
      import re
      import sys
      import json
      import argparse
      import hashlib
      import shutil
      from pathlib import Path
      from urllib.parse import unquote, urlparse
      from typing import List, Dict
      import subprocess
      import os
      
      
      def _load_verifier_runtime():
          """Load the repository kernel without requiring an editable install."""
          kernel_src = Path(__file__).resolve().parents[4] / 'packages' / 'bensz-skill-kernel' / 'src'
          if str(kernel_src) not in sys.path:
              sys.path.insert(0, str(kernel_src))
          from bensz_skill_kernel import Evidence, VerificationRequest, FilesystemVerifierRegistry, builtin_verifier_root, normalize_result, apply_gate, summarize_metrics
          return Evidence, VerificationRequest, FilesystemVerifierRegistry, builtin_verifier_root, normalize_result, apply_gate, summarize_metrics
      
      
      def get_skill_root() -> Path:
          """
          获取技能根目录的绝对路径。
      
          无论脚本从哪里调用,都能正确定位到技能根目录(包含 SKILL.md 的目录)。
      
          工作原理:
          1. 首先尝试从 __file__ 定位(脚本自身的绝对路径)
          2. 如果 __file__ 不可用(如某些执行环境),回退到环境变量
          3. 最后回退到当前工作目录下的 .claude/skills/{skill_name}
      
          Returns:
              技能根目录的绝对路径
          """
          # 方法1:通过 __file__ 定位(最可靠)
          if '__file__' in globals():
              # scripts/validate_links.py -> skills/{skill_name}/
              script_path = Path(__file__).resolve()
              # scripts/validate_links.py -> scripts/ -> {skill_name}/
              skill_root = script_path.parents[1]
              # 验证是否是有效的技能目录(包含 SKILL.md)
              if (skill_root / "SKILL.md").exists():
                  return skill_root
      
          # 方法2:通过环境变量定位(备用方案,支持自定义安装路径)
          env_skill_path = os.environ.get('VALIDATE_MD_REF Skill_PATH')
          if env_skill_path:
              skill_root = Path(env_skill_path).resolve()
              if (skill_root / "SKILL.md").exists():
                  return skill_root
      
          # 方法3:尝试从常见安装路径定位(回退方案)
          # 依次检查:用户级技能目录、项目级技能目录
          possible_paths = [
              Path.home() / ".claude" / "skills" / "validate-md-ref",
              Path.home() / ".codex" / "skills" / "validate-md-ref",
              Path.cwd() / ".claude" / "skills" / "validate-md-ref",
          ]
      
          for path in possible_paths:
              if (path / "SKILL.md").exists():
                  return path.resolve()
      
          # 方法4:如果都失败了,抛出错误并提供有用的诊断信息
          raise RuntimeError(
              f"无法定位 validate-md-ref 技能根目录。\n"
              f"请确认技能已正确安装到 ~/.claude/skills/ 或 ~/.codex/skills/\n"
              f"当前工作目录: {Path.cwd()}\n"
              f"__file__: {globals().get('__file__', '未定义')}\n"
              f"环境变量 VALIDATE_MD_REF Skill_PATH: {env_skill_path or '未设置'}"
          )
      
      
      # 预计算技能根目录(模块加载时执行一次)
      _skill_root = None
      
      
      def get_skill_root_cached() -> Path:
          """获取技能根目录(带缓存,避免重复计算)"""
          global _skill_root
          if _skill_root is None:
              _skill_root = get_skill_root()
          return _skill_root
      
      
      def get_config_path() -> Path:
          """获取默认配置文件的绝对路径"""
          return get_skill_root_cached() / "config.yaml"
      
      
      def validate_path(file_path: Path, base_dir: Path = None) -> bool:
          """
          验证文件路径是否安全(防止路径遍历攻击)
      
          注意:对于 URL 验证工具,允许验证任意可访问的文件,
          只需确保路径不包含明显的恶意模式(如 ../.. 逃逸)。
      
          Args:
              file_path: 用户指定的文件路径
              base_dir: 允许的基目录(默认为当前工作目录,但此处不强制限制)
      
          Returns:
              True 表示路径安全
          """
          try:
              # 规范化路径
              resolved = file_path.resolve()
      
              # 检查路径是否包含明显的路径遍历模式
              path_str = str(file_path)
              dangerous_patterns = ['../..', '../../', '..\\..']
              if any(pattern in path_str for pattern in dangerous_patterns):
                  return False
      
              # 检查路径是否尝试访问系统敏感目录
              resolved_str = str(resolved)
              sensitive_paths = ['/etc/', '/sys/', '/proc/', 'C:\\Windows\\System32']
              if any(resolved_str.startswith(sensitive) for sensitive in sensitive_paths):
                  return False
      
              # 确保文件存在
              if not resolved.exists():
                  return False
      
              return True
          except Exception:
              return False
      
      
      def _curl_probe(url: str, timeout: int, method: str) -> tuple[int, str]:
          cmd = ['curl', '-s', '-L', '-o', os.devnull, '-w', '%{http_code}\n%{url_effective}']
          if method == 'HEAD':
              cmd.append('-I')
          else:
              cmd.extend(['--range', '0-0'])
          cmd.extend(['--', url])
          output = subprocess.check_output(
              cmd,
              stderr=subprocess.DEVNULL,
              text=True,
              timeout=timeout,
          ).strip().split('\n')
          if not output or not output[0].isdigit():
              raise ValueError(f"无法解析 curl 响应: {output}")
          return int(output[0]), output[1] if len(output) > 1 and output[1] else url
      
      
      def validate_url(url: str, timeout: int = 10) -> Dict[str, any]:
          """
          验证单个 URL 的可达性
      
          Args:
              url: 要验证的 URL
              timeout: 超时时间(秒)
      
          Returns:
              包含验证结果的字典:
              {
                  'url': str,
                  'valid': bool,
                  'status_code': int,
                  'redirected': bool,
                  'final_url': str,
                  'error': str
              }
          """
          result = {
              'url': url,
              'valid': False,
              'status_code': None,
              'redirected': False,
              'final_url': url,
              'error': None
          }
      
          # 安全验证:确保 URL 格式合法,不包含 curl 选项
          try:
              parsed = urlparse(url)
              if not parsed.scheme or not parsed.netloc:
                  result['error'] = 'URL 格式非法'
                  return result
              if parsed.scheme not in ['http', 'https']:
                  result['error'] = '不支持的协议'
                  return result
              # 检查 URL 是否包含可疑的 curl 选项特征
              if '--' in url or url.startswith('-'):
                  result['error'] = 'URL 包含非法字符'
                  return result
          except Exception as e:
              result['error'] = f'URL 解析失败: {e}'
              return result
      
          try:
              status_code, final_url = _curl_probe(url, timeout, 'HEAD')
              if status_code in (403, 405):
                  status_code, final_url = _curl_probe(url, timeout, 'GET')
              result['status_code'] = status_code
              result['final_url'] = final_url
              result['redirected'] = final_url != url
              if 200 <= status_code < 400:
                  result['valid'] = True
              else:
                  result['error'] = f"HTTP {status_code}"
      
          except subprocess.CalledProcessError as e:
              result['error'] = f"执行失败: {e}"
          except subprocess.TimeoutExpired:
              result['error'] = f"超时(>{timeout}秒)"
          except Exception as e:
              result['error'] = str(e)
      
          return result
      
      
      def _markdown_anchor_ids(content: str) -> set[str]:
          anchors = set()
          for match in re.finditer(r'<(?:a|span|div)\b[^>]*(?:id|name)=["\']([^"\']+)["\']', content, re.IGNORECASE):
              anchors.add(match.group(1))
          seen_headings: Dict[str, int] = {}
          for line in content.splitlines():
              match = re.match(r'^\s{0,3}#{1,6}\s+(.+?)\s*#*\s*$', line)
              if not match:
                  continue
              heading = re.sub(r'<[^>]+>', '', match.group(1)).strip().lower()
              slug = re.sub(r'[^\w\- ]', '', heading, flags=re.UNICODE)
              slug = re.sub(r'[\s\-]+', '-', slug).strip('-')
              if not slug:
                  continue
              count = seen_headings.get(slug, 0)
              anchors.add(slug if count == 0 else f"{slug}-{count}")
              seen_headings[slug] = count + 1
          return anchors
      
      
      def validate_anchor(url: str, content: str) -> Dict[str, any]:
          anchor = unquote(url[1:])
          valid = anchor in _markdown_anchor_ids(content)
          return {
              'url': url,
              'valid': valid,
              'status_code': None,
              'redirected': False,
              'final_url': url,
              'error': None if valid else f'站内 anchor 不存在: {anchor}',
              'local_anchor': True,
          }
      
      
      def extract_references(content: str) -> List[Dict[str, any]]:
          """
          从 Markdown 内容中提取引用
      
          Args:
              content: Markdown 文件内容
      
          Returns:
              引用列表,每个引用包含:
              {
                  'index': int,           # 在文档中的位置
                  'type': str,            # 引用类型
                  'url': str,             # URL
                  'text': str,            # 链接文本或描述
                  'line_number': int,     # 行号
                  'full_match': str       # 完整匹配的文本
              }
          """
          references = []
      
          # 引用模式
          patterns = [
              # 标准链接: [文本](URL)
              {
                  'name': 'standard_link',
                  'pattern': r'\[([^\]]+)\]\(([^)]+)\)'
              },
              # HTML <a> 标签: <a href="URL">文本</a> 或 <a href='URL'>文本</a>
              {
                  'name': 'html_tag',
                  'pattern': r'<a\s+href=(["\'])([^"\']+)\1[^>]*>(.*?)</a>'
              },
              # 参考文献样式: [编号]: URL "描述"
              {
                  'name': 'bibliography',
                  'pattern': r'^\[(\d+)\]:\s*(\S+)\s*"?(.*?)"?$'
              },
              # 脚注样式: [^编号]: URL "描述"
              {
                  'name': 'footnote',
                  'pattern': r'^\^\[(\d+)\]:\s*(\S+)\s*"?(.*?)"?$'
              }
          ]
      
          lines = content.split('\n')
      
          for line_num, line in enumerate(lines, 1):
              for pattern_info in patterns:
                  pattern = pattern_info['pattern']
                  type_name = pattern_info['name']
      
                  if type_name in ('standard_link', 'html_tag'):
                      # 标准链接和 HTML 标签可能一行有多个
                      matches = re.finditer(pattern, line, re.IGNORECASE if type_name == 'html_tag' else 0)
                      for match in matches:
                          url = match.group(2).strip()
                          text = match.group(1).strip() if type_name == 'standard_link' else match.group(3).strip()
                          references.append({
                              'index': len(references),
                              'type': type_name,
                              'url': url,
                              'text': text,
                              'line_number': line_num,
                              'full_match': match.group(0)
                          })
                  else:
                      # 参考文献样式每行最多一个
                      match = re.match(pattern, line, re.MULTILINE)
                      if match:
                          ref_num = match.group(1)
                          url = match.group(2).strip()
                          desc = match.group(3).strip() if len(match.groups()) >= 3 else ''
      
                          references.append({
                              'index': len(references),
                              'type': type_name,
                              'reference_number': ref_num,
                              'url': url,
                              'text': desc,
                              'line_number': line_num,
                              'full_match': match.group(0)
                          })
      
          return references
      
      
      def should_skip_domain(url: str, whitelist: List[str], blacklist: List[str]) -> bool:
          """
          检查 URL 是否应该被跳过(基于域名白名单/黑名单)
      
          Args:
              url: 要检查的 URL
              whitelist: 域名白名单
              blacklist: 域名黑名单
      
          Returns:
              True 表示应该跳过验证
          """
          try:
              parsed = urlparse(url)
              domain = parsed.netloc.lower()
      
              # 检查黑名单
              for blocked in blacklist:
                  if blocked.startswith('*.'):
                      # 通配符匹配
                      suffix = blocked[2:]
                      if domain.endswith(suffix):
                          return True
                  else:
                      if domain == blocked.lower():
                          return True
      
              # 检查白名单
              if whitelist:
                  allowed = False
                  for allowed_domain in whitelist:
                      if domain == allowed_domain.lower() or domain.endswith('.' + allowed_domain.lower()):
                          allowed = True
                          break
                  return not allowed
      
              return False
          except Exception:
              return False
      
      
      def validate_references(references: List[Dict], config: Dict, content: str = '', base_dir: Path = None) -> List[Dict]:
          """
          批量验证引用
      
          Args:
              references: 引用列表
              config: 配置字典
      
          Returns:
              包含验证结果的引用列表
          """
          results = []
      
          whitelist = config.get('domain_whitelist', [])
          blacklist = config.get('domain_blacklist', [])
          timeout = config.get('validation', {}).get('timeout', 10)
          base_dir = Path(base_dir or Path.cwd()).expanduser().resolve()
      
          for ref in references:
              url = ref['url']
      
              if url.startswith('#'):
                  results.append({**ref, 'validation': validate_anchor(url, content)})
                  continue
      
              parsed = urlparse(url)
              if not parsed.scheme and not parsed.netloc:
                  relative_path = (base_dir / unquote(parsed.path)).resolve()
                  try:
                      relative_path.relative_to(base_dir)
                      in_scope = True
                  except ValueError:
                      in_scope = False
                  if not in_scope or not relative_path.is_file():
                      validation = {'url': url, 'valid': False, 'error': f'相对文件不存在或越界: {parsed.path}'}
                  elif parsed.fragment:
                      linked_content = relative_path.read_text(encoding='utf-8')
                      anchor_result = validate_anchor(f'#{parsed.fragment}', linked_content)
                      validation = {**anchor_result, 'url': url}
                  else:
                      validation = {'url': url, 'valid': True, 'local_file': True, 'error': None}
                  results.append({**ref, 'validation': validation})
                  continue
      
              # 检查是否应该跳过
              if should_skip_domain(url, whitelist, blacklist):
                  results.append({
                      **ref,
                      'validation': {
                          'skipped': True,
                          'reason': '域名在黑名单或不在白名单中'
                      }
                  })
                  continue
      
              # 验证 URL
              validation = validate_url(url, timeout)
              results.append({
                  **ref,
                  'validation': validation
              })
      
          return results
      
      
      def generate_summary(results: List[Dict]) -> Dict:
          """
          生成验证结果摘要
      
          Args:
              results: 验证结果列表
      
          Returns:
              摘要统计信息
          """
          total = len(results)
          valid = invalid = unresolved = timed_out = skipped = 0
          for item in results:
              validation = item.get('validation', {})
              status = validation.get('validation_status')
              if status == 'valid' or (status is None and validation.get('valid', False)):
                  valid += 1
              elif status == 'invalid' or (status is None and not validation.get('valid', False) and not validation.get('skipped', False)):
                  invalid += 1
              elif status in {'unresolved', 'timed_out'}:
                  unresolved += 1
                  timed_out += status == 'timed_out'
              elif status == 'skipped' or (status is None and validation.get('skipped', False)):
                  skipped += 1
      
          return {
              'total': total,
              'valid': valid,
              'invalid': invalid,
              'unresolved': unresolved,
              'timed_out': timed_out,
              'skipped': skipped,
              'valid_rate': f"{(valid / total * 100):.1f}%" if total > 0 else "0%"
          }
      
      
      def _kernel_command() -> tuple[list[str], dict[str, str]]:
          """Resolve a version-compatible kernel CLI without silently using stale ``bsk``."""
          script_root = Path(__file__).resolve()
          kernel_src = script_root.parents[4] / 'packages' / 'bensz-skill-kernel' / 'src'
          if kernel_src.is_dir() and (kernel_src / 'bensz_skill_kernel' / '__init__.py').is_file():
              env = os.environ.copy()
              env['PYTHONPATH'] = str(kernel_src) + (os.pathsep + env['PYTHONPATH'] if env.get('PYTHONPATH') else '')
              return [sys.executable, '-m', 'bensz_skill_kernel.cli'], env
      
          executable = shutil.which('bsk')
          if executable:
              probe = subprocess.run([executable, '--version'], capture_output=True, text=True, check=False)
              expected = None
              try:
                  config_text = get_config_path().read_text(encoding='utf-8')
                  match = re.search(r'(?ms)^runtime:\s*\n\s+kernel:\s*\n\s+name:\s*[^\n]+\n\s+version:\s*([0-9]+\.[0-9]+\.[0-9]+)\s*$', config_text)
                  expected = match.group(1) if match else None
              except (OSError, RuntimeError):
                  pass
              actual = probe.stdout.strip().splitlines()[-1].strip() if probe.stdout.strip() else ''
              if probe.returncode == 0 and (expected is None or actual == expected):
                  return [executable], os.environ.copy()
              detail = f'kernel CLI version mismatch (expected {expected or "declared"}, got {actual or "unknown"})'
              raise RuntimeError(detail)
          raise RuntimeError('bensz-skill-kernel CLI is unavailable; install a matching bsk or run from the source tree')
      
      
      def record_runtime_events(events_path: str, results: List[Dict], gate: Dict, request_id: str, attempt_id: str = 'default') -> Dict:
          """Compatibility helper for callers that already have normalized results."""
          command, env = _kernel_command()
          result_payload = [{**item, 'request_id': request_id} for item in results]
          gate_payload = {**gate, 'request_id': request_id}
          args = command + [
              'verification', events_path,
              '--result-json', json.dumps(result_payload, ensure_ascii=False, separators=(',', ':')),
              '--gate-json', json.dumps(gate_payload, ensure_ascii=False, separators=(',', ':')),
              '--scope', 'skill',
              '--actor', 'validate-md-ref',
              '--attempt-id', attempt_id,
              '--idempotency-key', request_id,
              '--run-id', request_id,
          ]
          completed = subprocess.run(args, capture_output=True, text=True, env=env, check=False)
          if completed.returncode != 0:
              detail = completed.stderr.strip() or 'bsk verification failed'
              raise RuntimeError(detail)
          return {'recorded': True, 'events': json.loads(completed.stdout)}
      
      
      def run_kernel_verifier(args) -> int:
          """Explain that generic citation verification needs normalized evidence."""
          print(json.dumps({
              'error': 'bensz.evidence.citation-truth-fit requires normalized subject_context, source_metadata and source_excerpt evidence; use this Markdown adapter without --kernel mode',
          }, ensure_ascii=False))
          return 2
      
      
      def main(argv=None):
          """主函数"""
          parser = argparse.ArgumentParser(description='验证 Markdown 引用并输出结构化结果')
          parser.add_argument('markdown_file')
          parser.add_argument('config_file', nargs='?')
          parser.add_argument('--events', help='通过 bsk verifier run 追加到指定 events.ndjson')
          parser.add_argument('--run-id', help='本次验证的稳定运行 ID')
          parser.add_argument('--attempt-id', default='default')
          parser.add_argument('--legacy-local', action='store_true', help=argparse.SUPPRESS)
          args = parser.parse_args(argv)
      
          if not args.markdown_file:
              print(json.dumps({
                  'error': '用法: validate_links.py <markdown_file> [config_file]'
              }))
              return 1
      
          md_file = Path(args.markdown_file)
      
          # 路径安全验证(防止路径遍历攻击)
          if not validate_path(md_file):
              print(json.dumps({
                  'error': f'路径不安全或超出允许范围: {md_file}'
              }))
              return 1
      
          if not md_file.exists():
              print(json.dumps({
                  'error': f'文件不存在: {md_file}'
              }))
              return 1
      
          # 读取 Markdown 内容
          content = md_file.read_text(encoding='utf-8')
      
          # 加载配置(自动使用默认配置或用户指定的配置)
          config = {}
          config_path = None
      
          if args.config_file:
              # 用户提供了配置文件路径
              config_path = Path(args.config_file)
          else:
              # 自动使用技能默认配置文件
              try:
                  config_path = get_config_path()
              except RuntimeError:
                  # 无法定位技能根目录时,使用空配置(不影响基本功能)
                  config = {}
                  config_path = None
      
          # 只有在配置文件路径存在时才加载
          if config_path:
              try:
                  import yaml
              except ImportError:
                  print(json.dumps({
                      'error': '未安装 yaml 库,请先安装:pip install pyyaml'
                  }))
                  return 1
      
              try:
                  if not config_path.exists():
                      print(json.dumps({
                          'error': f'配置文件不存在: {config_path}'
                      }))
                      return 1
      
                  with open(config_path, 'r', encoding='utf-8') as f:
                      config = yaml.safe_load(f) or {}
              except Exception as e:
                  print(json.dumps({
                      'error': f'加载配置失败: {e}'
                  }))
                  return 1
      
          # The kernel Markdown verifier is the single source of truth for collected
          # references and validation facts.  The legacy ``validate_references``
          # helper remains available for callers that imported it directly, but the
          # command-line entry point must not produce a second (and potentially less
          # secure) network-validation result set.
          output = {'file': str(md_file)}
      
          # The Markdown parser is an adapter. The verifier itself is format-agnostic
          # and receives normalized claim/source evidence instead of a Markdown file.
          try:
              Evidence, VerificationRequest, FilesystemVerifierRegistry, builtin_verifier_root, normalize_result, apply_gate, summarize_metrics = _load_verifier_runtime()
              runtime_decl = config.get('runtime', {}) if isinstance(config.get('runtime', {}), dict) else {}
              kernel_decl = runtime_decl.get('kernel', {}) if isinstance(runtime_decl.get('kernel', {}), dict) else {}
              if kernel_decl:
                  from bensz_skill_kernel import __version__ as running_kernel_version, validate_kernel_runtime_declaration
                  validate_kernel_runtime_declaration(kernel_decl, running_version=running_kernel_version)
              content_hash = hashlib.sha256(content.encode('utf-8')).hexdigest()
              request_id = args.run_id or f"markdown:{content_hash[:16]}"
              resolved_md_file = md_file.resolve()
              registry = FilesystemVerifierRegistry(builtin_verifier_root())
              declared = config.get('runtime', {}).get('verifiers', []) if isinstance(config.get('runtime', {}), dict) else []
              from bensz_skill_kernel import normalize_requirements
              requirements = list(normalize_requirements(declared or [
                  {'id': 'bensz.document.markdown-link-integrity', 'version': '1.0.0', 'required': True},
                  {'id': 'bensz.evidence.citation-truth-fit', 'version': '1.0.0', 'required': False},
              ], registry))
              requirement_by_id = {item['id']: item for item in requirements}
              if 'bensz.document.markdown-link-integrity' not in requirement_by_id:
                  raise ValueError('runtime verifier declaration must include bensz.document.markdown-link-integrity')
              link_requirement = requirement_by_id.get('bensz.document.markdown-link-integrity', {'version': '1.0.0'})
              citation_requirement = requirement_by_id.get('bensz.evidence.citation-truth-fit', {'version': '1.0.0'})
              link_result = registry.run(
                  'bensz.document.markdown-link-integrity',
                  {
                      'request_id': request_id,
                      'subject': {'type': 'file', 'path': str(resolved_md_file), 'content_hash': content_hash},
                      'context': {
                          'timeout': int(config.get('validation', {}).get('timeout', 10)),
                          'blacklist': list(config.get('domain_blacklist', []) or []),
                          'whitelist': list(config.get('domain_whitelist', []) or []),
                      },
                  },
                  version=link_requirement.get('version') or None,
              )
              link_facts = link_result.get('facts')
              if not isinstance(link_facts, dict) or not isinstance(link_facts.get('summary'), dict) or not isinstance(link_facts.get('references'), list):
                  raise RuntimeError('markdown-link-integrity returned no normalized facts')
              # Expose exactly the facts produced by the kernel Pack.  This keeps the
              # human-facing summary aligned with the required Verifier Gate and its
              # SSRF-safe redirect/hostname policy.
              results = link_facts['references']
              summary = link_facts['summary']
              output.update({'summary': summary, 'references': results})
              request = VerificationRequest(
                  subject={'type': 'citation', 'source_format': 'markdown', 'path': str(resolved_md_file), 'content_hash': content_hash},
                  requirements=('citation.semantic_review',),
                  evidence=(
                      Evidence('subject_context', 'markdown', {'path': str(md_file), 'content': content}),
                      Evidence('source_metadata', 'citation-list', {'references': results}),
                      Evidence('source_excerpt', 'validator', {'summary': summary, 'references': results}),
                  ),
                  request_id=request_id,
              )
              from bensz_skill_kernel import __version__ as kernel_version
              runtime_metadata = {
                  'kernel': {
                      'name': 'bensz-skill-kernel',
                      'version': kernel_version,
                      'source': 'source-tree' if Path(__file__).resolve().parents[4].joinpath('packages', 'bensz-skill-kernel').is_dir() else 'installed-package',
                  },
                  'pack_versions': {item['id']: item['version'] for item in requirements},
              }
              verifier_results = [{**link_result, 'runtime': runtime_metadata}]
              if 'bensz.evidence.citation-truth-fit' in requirement_by_id:
                  citation_result = registry.run(
                      'bensz.evidence.citation-truth-fit',
                      {
                          'request_id': request.request_id,
                          'subject': dict(request.subject),
                          'requirements': list(request.requirements),
                          'evidence': [item.to_dict() for item in request.evidence],
                          'context': dict(request.context),
                      },
                      version=citation_requirement.get('version') or None,
                  )
                  verifier_results.append({**citation_result, 'runtime': runtime_metadata})
              specs = {item['id']: registry.resolve(item['id'], item.get('version') or None).spec for item in requirements}
              for item in verifier_results:
                  verifier_id = item.get('verifier_id')
                  if verifier_id and verifier_id not in specs:
                      specs[verifier_id] = registry.resolve(verifier_id, item.get('verifier_version') or None).spec
              normalized = tuple(normalize_result(item, specs[item.get('verifier_id')], evidence_refs=item.get('evidence_refs', ())) for item in verifier_results if item.get('verifier_id') in specs)
              gate_decision = apply_gate(normalized, requirements=requirements)
              gate = gate_decision.to_dict()
              gate['requirements'] = requirements
              output['verification'] = {
                  'request_id': request.request_id,
                  'results': verifier_results,
                  'gate': gate,
                  'metrics': summarize_metrics(normalized, (gate_decision,), required_ids=[item['id'] for item in requirements if item.get('required')]),
                  'requirements': requirements,
                  'runtime': runtime_metadata,
              }
          except Exception as exc:
              print(json.dumps({'error': f'kernel verifier unavailable: {exc}'}, ensure_ascii=False))
              return 2
      
          if args.events and output.get('verification', {}).get('results'):
              try:
                  output['runtime'] = record_runtime_events(
                      args.events,
                      output['verification']['results'],
                      output['verification']['gate'],
                      request_id,
                      args.attempt_id,
                  )
              except Exception as exc:
                  output['runtime'] = {'recorded': False, 'error': str(exc)}
                  print(json.dumps(output, ensure_ascii=False, indent=2))
                  return 2
      
          print(json.dumps(output, ensure_ascii=False, indent=2))
          return 0
      
      
      if __name__ == '__main__':
          raise SystemExit(main())
      
  • CHANGELOG.md 12.5 KB
    # 变更日志
    
    本文件记录 validate-md-ref 技能的所有重要变更。
    
    格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),
    版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
    
    ---
    
    ## [Unreleased]
    
    ### Fixed(修复)
    - 升级至 `0.13.4`,运行时版本检查改用 BSK 的统一最低版本/capability 契约,避免托管最新版 Kernel 被旧精确版本声明错误拒绝。
    - 更新事件 QA:调用方 Gate 仅为 advisory;`unchecked` 结果以 Kernel 重算并持久化的 `wait` 为准,同时核验 `computed_by: kernel`。
    
    ### Changed(变更)
    - 升级至 `0.13.3` 并同步 `bensz-skill-kernel@0.14.0`,继续使用原有 canonical Verifier/State,同时兼容新的 Contract Pack 组件结果与 fail-closed Gate 绑定。
    - 同步 `bensz-skill-kernel` 至 `0.13.0`,支持 Skill 本地 Verifier Pack 合并发现和 `mode: prompt` 元数据。
    
    ### Fixed(修复)
    - 同步 `bensz-skill-kernel` 运行时版本至 `0.12.4`,使用缺失 required verifier 的 fail-closed Gate 和结构化非法请求错误。
    
    ### Changed(变更)
    - 运行时按 required/advisory requirements 计算 Gate,区分确定性链接失效与 DNS/连接/超时不可观测状态,并保留 instruction-only Verifier 的 evidence refs。
    - 运行身份要求 `run_id` 与 `attempt_id` 成对提供;状态转移追加可回放事件,状态快照采用稳定字段哈希并在读取/回放时核验漂移。
    - 运行时统一校验 Kernel 与 Verifier 版本、canonical ID、重复项和非法版本;Skill 升级至 `0.13.1`,匹配 `bensz-skill-kernel@0.12.1`。
    
    ### Fixed(修复)
    - `bsk rebuild` 对快照完整性失败返回结构化 `integrity_error`,删除缓存时仍以事件账本为准恢复投影。
    
    ### Changed(变更)
    - 对齐 `bensz-skill-kernel` 最新事件与指标协议:运行事件写入稳定 `run_id`,脚本结果新增 `verification.metrics`,保留原有 `summary`、`references`、`results` 和 `gate` 字段及旧 CLI 调用兼容性。
    
    ### Changed(变更)
    - 对齐 kernel 最新 `bensz-pack-index-v1`:新增状态包索引,并让 CLI 以 Markdown Verifier 的规范化 facts 作为唯一摘要与引用来源,避免旧本地网络验证与 kernel 安全策略分叉。
    - 补充执行契约:CLI 不再将兼容 API 的本地探测结果与 kernel Verifier 结果混合。
    - 版本升级至 `0.12.0`。
    - 将状态声明迁移到 `config.yaml.runtime`,状态包托管在 `references/states/`;旧 `state-machine.json` 保留兼容读取。
    - 脚本通过 kernel 同时执行 required 的 `bensz.document.markdown-link-integrity@1.0.0` 与 advisory 的引用语义 Verifier,并保留统一 Gate 结果。
    
    ### Changed(变更)
    - **State ID 对齐**:Skill 升级至 `0.10.0`,状态声明、入口条件与迁移边统一使用 `bensz.workspace.*` 和 `bensz.validate-md-ref.*` canonical ID;旧 ID 作为兼容 alias。
    - **Verifier ID 对齐**:Skill 升级至 `0.9.0`,统一使用 `bensz.document.markdown-link-integrity` 与 `bensz.evidence.citation-truth-fit` canonical ID;旧 ID 仅作为兼容 alias。
    - **SKILL.md 精简**:合并重复说明并压缩执行契约,保留引用检查范围、Kernel 状态机与 Verifier 门禁、输入输出、安全边界及工具命令。
    
    ### Changed(变更)
    - **职责边界收敛**:将 `SKILL.md` 从状态机和 Verifier 的手工操作手册收敛为 Markdown 引用核查的业务契约;仅用自然语言声明运行时门禁,具体协议保留在 references,避免模型绕过执行器自行编排底层流程。
    - **强制状态机与 Verifier**:Skill 升级至 `0.8.2`。每次运行必须完成 `input-ready`、`checking`、`reported` 状态转移,并运行 `markdown.link-integrity@1.0.0` 与事件账本记录;kernel、转移或 Verifier 不可用时明确失败,禁止静默降级。
    - **受众定位修正**:重写 `SKILL.md` 的执行说明,去除面向 Skill 开发者的内部措辞,改为面向用户和 AI 的输入边界、工具选择、参考资料和结果汇总指引;保留协议细节在 `references/` 中维护。
    - **状态机与 Verifier 文档分层**:Skill 升级至 `0.8.1`;将状态机操作、状态不变量和 Verifier 调用边界迁移至 `references/state-machine.md` 与 `references/verifiers.md`,使 `SKILL.md` 聚焦 Markdown 输入适配、事实采集和结果汇总。
    
    ### Changed(变更)
    - **状态机协议试点**:Skill 升级至 `0.8.0`,新增 `state-machine.json` 与目录化元状态包;以 `workspace.ready` 为强制初始状态,并通过 `bsk state` 的标准 JSON 操作结果管理输入准备、核验与报告阶段。
    
    ### Changed(变更)
    - **接入目录化 verifier 协议**:Skill 升级至 `0.7.0`,新增 `markdown.link-integrity@1.0.0` 调用说明;kernel 负责发现、执行和标准化结果,Skill 继续负责 Markdown 配置适配。
    - **Verifier 定位修正**:将引用 Verifier 定义为不受文档类型限制的 `citation.truth-and-fit`;Markdown 仅负责输入适配和事实采集,Verifier 要求论断上下文、来源元数据和来源摘录。Skill 版本升级至 `0.6.0`。
    - **恢复 BenszAPI 任务工作区约定**:在 `SKILL.md` 中补回任务级 `./.bensz-api/task-{yyyymmdd-hhmm}-{简短描述}/validate-md-ref/input|output|log/` 目录契约,明确共享材料、正式交付物分流、敏感信息边界与 legacy 路径规则。
    - **SKILL.md 执行契约增强**:补充适用范围、输入输出、最小执行流程和相对路径调用说明;明确脚本与直接 verifier 的配置加载差异,并将触发描述改为同时说明能力和使用场景。
    - **Verifier ID 与版本解耦**:调用从 `markdown.references.v1` 改为稳定 ID `markdown.references`,显式固定 Pack 版本 `1.0.0`;Skill 版本升级至 `0.5.0`。
    - **Skill 使用说明简化**:将 SKILL.md 收敛为工具包、任务到命令的映射和 references 索引;移除状态机、Gate 和复杂验证流程说明,references 改为简短的工具与输入输出定义。
    - **Skill 文档分层重构**:description 只保留面向用户的触发条件,正文改为说明检查目标、输入输出和解释边界;移除 `verifier` 关键词,避免把实现工具误当作 Skill 特性。
    - **参考资料按需加载**:新增 `references/` 下的引用范围、运行配置、结果字段和网络安全说明,减少 SKILL.md 对运行时细节的承载。
    - **版本**:Skill `0.4.1 → 0.4.2`。
    
    ### Fixed(修复)
    - **重定向安全边界**:kernel verifier 改为逐跳检查重定向目标的协议、白名单、显式黑名单与私网地址,拒绝目标不会被请求;`SKILL.md` 和 README 同步说明该约束。
    
    ### Changed(变更)
    - **公开调用契约**:明确 `bsk` runtime/verifier 的预检、事件账本的写入副作用,以及直接调用时应通过 CLI 参数传入超时和域名策略;需要 YAML 配置时使用脚本封装。
    - **配置收敛**:移除未被公开运行时消费的重定向、User-Agent 与输出配置项,仅保留实际生效的超时、域名白名单和黑名单。
    - **版本**:Skill `0.4.0 → 0.4.1`。
    
    ### Changed(变更)
    - **SKILL.md 结构收敛**:description 仅保留触发条件;正文移除重复一级标题,并将范围、调用方式和验证器边界分为独立小节。
    - **Kernel Verifier 接入**:验证脚本改为薄封装,调用内置 `bsk verifier run markdown.references.v1`;Skill 只声明命令、所需 verifier 和标签,`--events`、`--run-id`、`--attempt-id` 用于可选审计记录。
    - **Verifier 声明收敛**:移除 Skill 目录中未被 Runtime 消费的 Pack YAML、校准样例和重复 Python 注册器;脚本回退路径直接使用 kernel 内置 registry,避免多份来源漂移。
    
    ### Added(新增)
    - 接入 `bensz-skill-kernel` Verifier Pack:以版本化 `hybrid` 契约输出证据快照、规则结果、语义检查缺口与保守 Gate 决策;保留原有 JSON 字段以兼容既有调用方。
    
    ### Fixed(修复)
    - 站内 `#anchor` 改为在当前 Markdown 的显式 HTML anchor 与标题 slug 中本地校验,不再作为非法外部 URL 计入失败。
    - 外部 URL 的 HEAD 请求返回 403/405 时执行一次有限 GET 回退,避免把“禁止 HEAD、允许 GET”的页面误判为不可访问;版本号 `0.2.0 → 0.2.1`。
    
    ### Added(新增)
    - HTML `<a>` 标签支持:识别和验证 HTML 格式的超链接(`<a href="URL">文本</a>`)
    
    ### Changed(变更)
    - **SKILL.md**:
      - 更新引用模式说明,增加 HTML `<a>` 标签描述
      - 更新"当前实现范围"到 v0.2.0
    - **scripts/validate_links.py**:
      - 新增 `html_tag` 引用类型
      - 正则表达式支持单引号和双引号包裹的 href 值
      - 不区分大小写匹配 `<a>` 标签(`<A>`、`<a>` 均可识别)
    
    ---
    
    ## [0.1.1] - 2026-01-18
    
    ### Added(新增)
    - 跨目录路径定位:脚本自动定位技能根目录,支持从任意工作目录调用
    - 多层回退机制:通过 `__file__`、环境变量、常见路径探测自动定位配置文件
    - 路径安全策略优化:允许验证任意可访问文件,同时防止路径遍历攻击
    
    ### Changed(变更)
    - **scripts/validate_links.py**:
      - 新增 `get_skill_root()` 和 `get_skill_root_cached()` 函数实现自动路径定位
      - 新增 `get_config_path()` 函数自动获取默认配置文件路径
      - 优化 `validate_path()` 函数:放宽路径限制,允许跨目录验证文件
      - 配置文件加载逻辑:默认使用技能内 config.yaml,无需手动指定
    - **SKILL.md**:
      - 更新工作流程说明:明确 `python3 scripts/validate_links.py` 调用方式
      - 新增"技术实现"章节:详细说明自动路径定位机制
      - 更新"输入要求":说明配置文件自动加载机制
      - 更新"当前实现范围":标注自动配置加载功能
    
    ### Fixed(修复)
    - **P0**:跨目录调用失败 - 相对路径 `scripts/validate_links.py` 在不同工作目录下无法解析
    - **P0**:配置文件路径硬编码 - 必须手动指定配置文件路径,缺乏灵活性
    
    ---
    
    ## [0.1.1] - 2026-01-18
    
    ### Added(新增)
    - URL 安全验证:检查 URL 格式,防止命令注入攻击
    - 路径遍历防护:使用 `Path.resolve()` 验证文件路径在允许范围内
    - 跨平台超时机制:使用 `subprocess.check_output(timeout=)` 替代 `signal.SIGALRM`
    - 错误处理改进:yaml 加载失败时输出明确的错误提示
    
    ### Changed(变更)
    - **SKILL.md**:
      - 聚焦到已实现的功能(引用提取 + URL 验证)
      - 删除未实现功能的描述(内容对比、无效链接处理、引用重编号)
      - 增加"注意事项"说明当前实现范围
      - 更新输出规范,使用实际的 JSON 格式
    - **config.yaml**:
      - 删除 `concurrent_checks` 配置(未实现)
      - 删除 `renumbering` 配置(未实现)
      - 删除 `invalid_link_action` 和 `placeholder_text` 配置(未实现)
      - 删除 `content_comparison` 配置(未实现)
      - 删除 `reference_patterns` 配置(与代码重复)
      - 简化为只包含实际生效的配置项
    - **scripts/validate_links.py**:
      - 移除 `signal` 模块依赖(Windows 不兼容)
      - 使用 `subprocess.TimeoutExpired` 捕获超时
      - 改进配置加载逻辑(空配置文件处理)
      - 增加路径验证函数 `validate_path()`
    
    ### Fixed(修复)
    - **P0-1**:命令注入风险 - URL 未经验证直接传递给 curl
    - **P0-2**:路径遍历风险 - 未验证文件路径是否在允许范围内
    - **P0-3**:架构缺陷 - 工作流描述与实际实现严重脱节
    - **P1-1**:过度设计 - `concurrent_checks` 配置项未实现
    - **P1-2**:过度设计 - `renumbering.format` 配置项无效
    - **P1-3**:一致性问题 - SKILL.md 与 config.yaml 的无效链接处理方式描述不一致
    - **P1-4**:冗余检查 - `content_comparison` 配置项无效
    - **P1-5**:跨平台兼容性 - `signal.SIGALRM` 在 Windows 上不可用
    - **P1-6**:错误处理不完整 - config.yaml 加载失败时降级到空配置
    - **P1-7**:一致性 - `reference_patterns` 配置与实际代码重复
    - **Bug**:解析 HTTP 状态码时出错(curl 输出格式解析有误)
    
    ### Security(安全)
    - URL 格式验证:防止命令注入攻击
    - 路径规范化:防止路径遍历攻击
    - 协议限制:仅支持 http/https
    
    ---
    
    ## [0.1.0] - 2026-01-13
    
    ### Added(新增)
    - 初始化技能,实现核心功能
    - 引用提取:支持标准链接、参考文献、脚注格式
    - URL 验证:使用 curl 检查可达性
    - 域名过滤:支持白名单/黑名单配置
    
  • config.yaml 1.3 KB
    # validate-md-ref 技能配置文件
    
    # 技能基本信息
    skill_info:
      name: validate-md-ref
      version: 0.13.4
      description: 验证 Markdown 文档中链接的完整性与 URL 可访问性
      author: "Bensz Conan"
      category: 文档质量
    
    # URL 验证配置
    validation:
      # 请求超时时间(秒)
      timeout: 10
    
    # 域名白名单(空列表表示不限制)
    # 添加域名到白名单以只验证特定域名
    # 示例: ["github.com", "stackoverflow.com", "docs.python.org"]
    domain_whitelist: []
    
    # 域名黑名单(这些域名的链接将被跳过)
    # 示例: ["localhost", "127.0.0.1", "internal.company.com"]
    domain_blacklist:
      - localhost
      - 127.0.0.1
      - 0.0.0.0
      - "*.local"
      - "*.internal"
    
    # Kernel runtime declaration.  The legacy state-machine.json remains readable
    # for older installations, while new callers discover this single declaration.
    runtime:
      kernel:
        name: bensz-skill-kernel
        version: 0.14.1
      state_roots: [references/states]
      initial_state: bensz.workspace.ready
      states:
        - bensz.validate-md-ref.input-ready
        - bensz.validate-md-ref.checking
        - bensz.validate-md-ref.reported
      verifiers:
        - id: bensz.document.markdown-link-integrity
          version: 1.0.0
          required: true
        - id: bensz.evidence.citation-truth-fit
          version: 1.0.0
          required: false
    
  • README.md 6.3 KB
    # validate-md-ref
    
    当前版本:`0.13.4`。这个 skill 将 Markdown 作为输入适配层,提取引用并采集 URL/锚点事实,再交由目录化 verifier 协议判断引用完整性与引用真实性。每次运行都会强制经过使用 canonical State ID 的 kernel 状态机并执行 canonical Verifier;它不把链接可达性冒充语义结论,也不自动修改原文档。
    
    它适合跨格式引用核验;Markdown 只是当前可用的输入适配器。语义引擎缺口会明确标为 `unchecked` 或 `manual_review`。调用脚本时请使用 Bensz 托管运行时的 Python;`runtime.kernel.version` 是最低兼容版本,新版 Kernel 还会核验声明的 capabilities。
    
    执行细节按需阅读:
    
    - [工具包与命令](references/tools.md)
    - [输入与输出](references/formats.md)
    - [Verifier 契约与边界](references/verifiers.md)
    - [状态机契约](references/state-machine.md)
    
    ## 用法
    
    ### 最推荐用法
    
    ```text
    请使用 validate-md-ref skill 验证这个 Markdown 文档中的 URL 引用是否可访问。
    输入:`/path/to/file.md`
    输出:JSON 格式的结构化验证结果,包含有效、无效和跳过的链接统计
    ```
    
    ### 进阶用法
    
    ```text
    请使用 validate-md-ref skill 检查这个 Markdown 文档的 URL 引用。
    输入:`/path/to/file.md`
    输出:验证结果
    另外,还有下列参数约束:
    - 使用自定义配置文件:`custom-config.yaml`
    - 结果里按引用类型分类
    - 保留失败原因
    ```
    
    ## 能做什么
    
    - 提取 Markdown 里的多种 URL 引用形式。
    - 对每个 URL 做可达性检查和安全校验。
    - 输出结构化结果,方便后续人工或 AI 进一步处理。
    - 适合文档质检、交付前巡检、链接清单复核。
    - 不负责自动改写正文内容,也不直接替你决定如何处理无效链接。
    
    ## 使用示例
    
    ### 示例 1:检查单个 Markdown
    
    ```text
    请使用 validate-md-ref skill 验证这个 Markdown 文档中的 URL 引用。
    输入:`README.md`
    输出:JSON 结构化验证结果
    ```
    
    ### 示例 2:按自定义规则检查
    
    ```text
    请使用 validate-md-ref skill 检查这个 Markdown 文件。
    输入:`docs/review.md`
    输出:验证结果
    另外,还有下列参数约束:
    - 使用自定义配置文件:`validate-md-ref/config.yaml`
    ```
    
    ### 示例 3:为交付做最后巡检
    
    ```text
    请使用 validate-md-ref skill 检查这份 Markdown 交付文档的引用质量。
    输入:`deliverable.md`
    输出:有效、无效和跳过链接的结构化结果
    ```
    
    ## 输出
    
    - 核心输出是结构化验证结果,可继续转成 Markdown 报告。
    - 常见结果字段包括:
      - `summary.total`
      - `summary.valid`
      - `summary.invalid`
      - `summary.skipped`
      - `references[*].validation`
    - 当前脚本直接把 JSON 结果输出到标准输出,不会自动生成独立 Markdown 报告文件。
    - 使用状态机执行器时产生 `log/meta-state.json` 状态快照;传入 `--events` 时产生 `log/events.ndjson` Verifier 事件账本。直接脚本调用不会隐式创建任务工作区,若任务要求审计必须显式提供这些入口。
    - `verification.results` 保存原子规则结果与证据引用;本 Skill 的 `verification.gate` 用 `allow` 或 `reject` 表达链接完整性结果。格式无关的语义 Pack 才用 `manual_review` 表达验证缺口。
    - `verification.metrics` 保存 Kernel 计算的 Verifier 覆盖率、未知/不确定比例、Gate 放行率、assurance tier 与耗时指标。
    
    ## 配置
    
    - 配置文件:`validate-md-ref/config.yaml`
    - 默认超时:`10` 秒
    - 重定向由 kernel 以固定上限逐跳处理,并在每一跳发起请求前重新执行安全检查。
    - 支持域名白名单和黑名单。
    - 关键配置节:
      - `validation`
      - `domain_whitelist`
      - `domain_blacklist`
      - `runtime`:声明 `references/states` 状态包及链接/语义 Verifier 版本;状态包使用 `references/states/index.json` 的 `bensz-pack-index-v1` 清单
    
    Verifier 契约由 `bensz-skill-kernel` 内置 registry 统一维护;本 Skill 只声明调用方式和验证边界。
    
    `bensz.evidence.citation-truth-fit` 是唯一的引用 Verifier,不受文档类型限制;本 Skill 负责将 Markdown 转成它所需的标准证据。旧 ID `citation.truth-and-fit` 仅作为兼容 alias。详见 [引用真实性与适切性契约](references/citation-truth-and-fit.md)。
    
    直接调用 `bsk` 时,配置文件不会自动加载。请通过格式适配器提交结构化证据,再调用 `bensz.evidence.citation-truth-fit@1.0.0`;不能把文档路径直接当作通用语义输入。
    
    ## 备选用法(脚本/硬编码)
    
    如果你已经知道要检查哪个 Markdown 文件,直接调用脚本就可以得到结构化结果。
    
    ### 使用默认配置
    
    ```bash
    python3 validate-md-ref/scripts/validate_links.py README.md
    ```
    
    ### 指定自定义配置
    
    ```bash
    python3 validate-md-ref/scripts/validate_links.py \
      docs/review.md \
      validate-md-ref/config.yaml
    ```
    
    ### 调用 kernel 内置 Verifier
    
    这个 Skill 通过 runtime 声明链接完整性为 required、引用语义为 advisory,并保留两个 Verifier 的独立结果:
    
    ```bash
    bsk verifier list --tag common
    bsk verifier describe bensz.document.markdown-link-integrity --version 1.0.0
    bsk verifier describe bensz.evidence.citation-truth-fit --version 1.0.0
    ```
    
    所需 verifier:`bensz.evidence.citation-truth-fit`,版本 `1.0.0`。它带有 `common`、`citation`、`semantic`、`evidence` 标签;格式适配器负责提供标准证据,Verifier 输出统一的 `verification.results` 与 `verification.gate`。
    
    ## 常见问题
    
    ### Q:它会自动把无效链接从 Markdown 中删掉吗?
    
    A:不会。它负责“检查并报告”,后续是否删除、替换或标注,需要你或 AI 继续判断。
    
    ### Q:为什么结果里会出现 `skipped`?
    
    A:因为脚本会根据白名单和黑名单跳过某些域名验证,例如本地地址、内网地址,或不在白名单范围内的域名。
    
    ### Q:为什么有些本地或内网地址会被跳过?
    
    A:默认黑名单会排除 `localhost`、`127.0.0.1`、`*.local`、`*.internal` 等域名,避免把本地开发环境误当成公开可访问链接。
    
    ### Q:它能检查 Markdown 以外的格式吗?
    
    A:Markdown 只是当前 Skill 的输入适配器。Verifier `bensz.evidence.citation-truth-fit` 本身不限制 Markdown,LaTeX、Word 或其它格式适配器都可以提交同样的标准证据。
    
  • SKILL.md 7.7 KB
    ---
    name: validate-md-ref
    description: 检查 Markdown 文档中的引用是否可定位、URL 或锚点是否可访问,并整理供后续判断引用真实性与适切性的结构化证据。当用户要求核查引用、检查文档链接,或确认引用是否支持正文论断时使用。
    metadata:
      author: Bensz Conan
      short-description: 检查 Markdown 引用是否可定位、可访问
      keywords:
        - validate-md-ref
    ---
    
    # validate-md-ref
    
    ## 目标
    
    检查 Markdown 文档中的引用是否可定位、URL 或锚点是否可访问,并整理供后续判断引用真实性与适切性的结构化证据。当用户要求核查引用、检查文档链接,或确认引用是否支持正文论断时使用。
    
    ## 流程
    
    ### 输入
    
    #### 范围与边界
    
    - 输入:一个 Markdown 文件,可选一个 YAML 配置文件。
    - 检查:Markdown 行内链接、HTML `<a href>` 链接、当前文档内的 `#anchor`,以及外部 HTTP(S) 链接的可达性。
    - 输出:结构化 JSON,逐条保留引用位置、验证状态和失败或跳过原因。
    - 不做:不修改原文;URL 可达性不等于来源支持论断;不代替用户决定修复方式。
    - 运行时能力:使用 `bensz.document.markdown-link-integrity` 检查链接事实,并保留 `bensz.evidence.citation-truth-fit` 的语义复核状态;两者版本独立记录,旧 ID 仅作兼容 alias。
    
    ### 执行步骤
    
    #### 强制运行门禁
    
    每次执行必须经过 Bensz Skill Kernel 状态机并调用指定版本的链接完整性 Verifier。任一环节不可用或失败,任务即未完成并须说明原因;不得降级为普通脚本或手工检查。
    
    状态机使用 `bensz.workspace.ready` 作为系统入口,并依次进入 `bensz.validate-md-ref.input-ready`、`bensz.validate-md-ref.checking` 和 `bensz.validate-md-ref.reported`;旧 State ID 仅作兼容 alias。
    
    AI 应使用本 Skill 的执行器或封装入口,不得手工模拟状态转移、拼接事件账本或复制 Verifier 规则。命令、上下文和事件契约见:
    
    - [`references/state-machine.md`](references/state-machine.md)
    - [`references/verifiers.md`](references/verifiers.md)
    
    #### 流程
    
    1. 确认 Markdown 存在并只读处理;需限制网络请求时加载默认或指定 YAML。
    2. 提取并分类检查:站内锚点在当前文档定位;HTTP(S) 按安全策略验证可达性;白/黑名单命中则跳过。
    3. 保留链接 Verifier 标准结果;真实性或适切性仅保留语义 Verifier 的 `unchecked`/`manual_review`,无证据不得判通过。
    4. 汇总总数、有效、无效、跳过项,逐条披露位置、状态、失败原因和语义边界。
    
    网络 DNS、连接失败和超时属于 `unresolved`/`timed_out` 观测不确定性,不得当作确定性链接失效;只有 HTTP 明确错误、本地 anchor 缺失或越界文件才计入 `invalid`。
    
    命令行入口以 kernel `bensz.document.markdown-link-integrity` Pack 返回的
    `facts.summary` 与 `facts.references` 为唯一链接事实来源;不要将旧兼容函数的本地
    探测结果与 Verifier 结果合并或互相覆盖。
    
    #### 工具
    
    - `scripts/validate_links.py`:读取 Markdown 及可选 YAML,输出结构化结果。
    - `config.yaml`:提供默认超时、域名白名单和黑名单,并在 `runtime` 节声明状态包与 Verifier 选择。
    
    从 Skill 目录调用脚本;工作目录不同则使用绝对路径或先切换目录。运行入口负责状态机和 Verifier 门禁,不得绕过门禁解释脚本结果。
    
    常用业务调用形式:
    
    ```bash
    python3 scripts/validate_links.py DOCUMENT.md
    python3 scripts/validate_links.py DOCUMENT.md CONFIG.yaml
    ```
    
    输出字段和配置字段的完整说明见 [`references/formats.md`](references/formats.md) 与 [`references/tools.md`](references/tools.md)。
    
    ### 输出
    
    输出结构化引用检查结果、可定位证据和报告:记录每条引用的来源、URL/锚点状态、错误或不确定原因,以及供后续真实性/适切性判断使用的 Evidence 快照;不修改源 Markdown。
    
    ### 输出管理
    
    #### 文件边界
    
    需落盘时,将本 Skill 的输入、临时结果和日志写入当前会话声明的 `./.bensz-api/task-{yyyymmdd-hhmm}-{简短描述}/validate-md-ref/{input,output,log}/`;多 Skill 共享材料放任务根目录 `shared/`。正式交付物、用户指定文件和源 Markdown 留在项目约定位置。不得写入密钥、令牌、Cookie、私有指令、隐私或不必要的大体积原始数据;纯文本答复无需建目录。
    
    ### 校验
    
    校验输入路径位于允许的 `base_dir`、拒绝越界/symlink 逃逸和敏感路径,按配置检查 URL/锚点、重定向、域名白黑名单和超时;required 链接完整性通过后才可放行,advisory 真实性判断仅作提示并保留人工复核。
    
    ### 失败与恢复
    
    文件不可读、路径越界、URL/锚点检查超时或外部站点不可用时,保留已收集的 Evidence、错误分类和日志,按 required/advisory 规则阻断或标记 `uncertain/unchecked`;修复输入或网络后可在同一任务工作区重试,不把缺失证据视为通过。
    
    
    ## 控制
    
    运行时由 `bensz-skill-kernel` 按 `config.yaml.runtime` 管理 State、Verifier 与 Gate;链接完整性为 required,语义真实性为 advisory,失败或不确定时保留证据并转人工复核。
    
    ## 约束
    
    <!-- BEGIN COMMON CONSTRAINTS -->
    <!-- Source-Hash: sha256:15120201e9e0c7569517261d57ecefb63ac279c26ed13876f8e95b6dc35854d3 -->
    <!-- Template-ID: skill-common-constraints; Template-Version: 1; Sync-Policy: exact-block -->
    
    ### 公共硬约束
    
    本块由 `docs/templates/skill-common-constraints.md` 统一维护;每个 `SKILL.md` 的 `## 约束` 必须逐字同步本块,不得在副本中改写公共规则。
    
    - 任务需要落盘时,使用唯一的 `./.bensz-api/task-{yyyymmdd-hhmm}-{简短描述}/` 根目录;共享材料放入 `shared/`,Skill 专属材料放入该 Skill 的 `input/`、`output/`、`log/`。
    - 正式交付物、源代码和正式计划按项目约定保存,不写入任务工作区;未经授权不覆盖、删除、迁移或远程写入。
    - 项目维护变更检查 BAC 可用性并记录需求、AI 产出、工具结果、文件改动和验证摘要;BAC 只做过程审计,不替代署名、责任或合规判断。
    - 不记录 API Key、访问令牌、密码、Cookie、环境/凭据文件、私有 Prompt、身份信息、本地用户名、主机名或不必要的大体积原始数据。
    - 文件路径必须规范化并限制在授权项目范围内;外部 URL、子进程和网络访问遵循最小权限,防止路径遍历、SSRF 和命令注入。
    - Skill 版本唯一记录在自身 `config.yaml:skill_info.version`;公开 API、协议、目录或配置变更同步文档与 `CHANGELOG.md`。
    - `bensz-collect-bugs` 是一个 Agent Skill;仅将 Bensz Agent Skill 或 Bensz 基础设施本身的设计缺陷交给它。先脱敏写入 `~/.bensz-skills/bugs/`,当前任务不中断,只有用户明确要求才公开上报,禁止直接修改用户已安装的 Skill 源码。
    
    <!-- End of canonical common constraints. -->
    <!-- END COMMON CONSTRAINTS -->
    
    ### Skill 专属约束
    
    #### 疑似 Skill 设计问题
    
    - **适用范围**:仅记录流程漏判、输入约定不完整、环境假设错误等 Skill 设计缺陷;用户数据错误、第三方波动和偶发模型输出除外。
    - **隐私保护**:不得记录密钥、密码、身份信息、邮箱、私密路径、用户名、主机名或工作目录;公开前须脱敏。
    - **本地优先**:先写入 `~/.bensz-skills/bugs/`,不打断任务;仅用户明确要求时用本机 `gh api` 上报。
    - **禁止就地修 bug**:不要直接修改用户本地已安装 Skill 源码;先记录,再继续任务。
    
  • state-machine.json 260 B
    {
      "protocol": "bensz-skill-state-v1",
      "initial_state": "bensz.workspace.ready",
      "state_roots": ["references/states"],
      "states": [
        "bensz.validate-md-ref.input-ready",
        "bensz.validate-md-ref.checking",
        "bensz.validate-md-ref.reported"
      ]
    }
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related