📝 docs(tsl): rebuild canonical syntax and routing manual

This commit is contained in:
csh
2026-04-22 16:59:20 +08:00
parent 3ed5052e61
commit 96b705b00b
83 changed files with 46446 additions and 323 deletions
+285
View File
@@ -0,0 +1,285 @@
param(
[string]$RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
)
$ErrorActionPreference = "Stop"
$sourceDir = Join-Path $RepoRoot "data/tsl_reference_catalog_source"
$outputDir = Join-Path $RepoRoot "docs/tsl/reference/catalog"
$moduleOrder = @(
[pscustomobject]@{ File = "base"; Summary = "字符串、数组、日期时间、类型转换与常用基础能力" }
[pscustomobject]@{ File = "math"; Summary = "数值计算、统计分析、矩阵处理与数学算法" }
[pscustomobject]@{ File = "system"; Summary = "数据类型、表达式调用、性能与运行时能力" }
[pscustomobject]@{ File = "resource"; Summary = "文件、数据库、网络与外部资源访问" }
[pscustomobject]@{ File = "platform"; Summary = "平台相关功能与系统接口" }
[pscustomobject]@{ File = "client"; Summary = "客户端交互、界面控制与前端协作能力" }
[pscustomobject]@{ File = "graphics"; Summary = "图表、绘图与可视化相关函数" }
[pscustomobject]@{ File = "compression"; Summary = "压缩、解压与归档能力" }
[pscustomobject]@{ File = "digest_encoding"; Summary = "哈希、摘要、编码与转换能力" }
[pscustomobject]@{ File = "third_party"; Summary = "第三方库与外部程序交互能力" }
)
function Get-HeadingSections {
param([string[]]$Lines)
$sections = @()
for ($i = 0; $i -lt $Lines.Count; $i++) {
if ($Lines[$i] -match '^(#{4,7})\s+(.+?)\s*$') {
$sections += [pscustomobject]@{
Level = $Matches[1].Length
Title = $Matches[2].Trim()
Start = $i
}
}
}
for ($i = 0; $i -lt $sections.Count; $i++) {
$nextStart = if ($i + 1 -lt $sections.Count) { $sections[$i + 1].Start } else { $Lines.Count }
$sections[$i] | Add-Member -NotePropertyName End -NotePropertyValue ($nextStart - 1)
}
return $sections
}
function Test-IsFunctionSection {
param(
[string]$Title,
[string[]]$BodyLines
)
if ($null -eq $BodyLines -or $BodyLines.Count -eq 0) {
return $false
}
$body = $BodyLines -join "`n"
$hasStructuredMarkers = (
$body -match '(^|\n)\s*用途:' -or
$body -match '(^|\n)\s*参数:' -or
$body -match '(^|\n)\s*返回:'
)
if ($hasStructuredMarkers) {
return $true
}
$looksLikeFunctionName = $Title -match '^[A-Za-z_][A-Za-z0-9_]*$'
$hasMeaningfulBody = $body -match '\S'
return (
$looksLikeFunctionName -and
$hasMeaningfulBody
)
}
function Add-FunctionToGroup {
param(
[System.Collections.Specialized.OrderedDictionary]$GroupMap,
[string]$Category,
[string]$Subcategory,
[string]$FunctionName
)
if ([string]::IsNullOrWhiteSpace($FunctionName)) {
return
}
$categoryName = if ([string]::IsNullOrWhiteSpace($Category)) { "未分类" } else { $Category }
$groupTitle = if ([string]::IsNullOrWhiteSpace($Subcategory)) {
$categoryName
}
else {
"{0} / {1}" -f $categoryName, $Subcategory
}
if (-not $GroupMap.Contains($groupTitle)) {
$GroupMap[$groupTitle] = [pscustomobject]@{
Title = $groupTitle
Category = $categoryName
Subcategory = $Subcategory
Functions = [System.Collections.Generic.List[string]]::new()
}
}
$functionList = $GroupMap[$groupTitle].Functions
if (-not $functionList.Contains($FunctionName)) {
$functionList.Add($FunctionName)
}
}
function Parse-TslFunctionModule {
param([string]$Path)
$lines = Get-Content -LiteralPath $Path
$sections = Get-HeadingSections -Lines $lines
$moduleTitle = ($sections | Where-Object { $_.Level -eq 4 } | Select-Object -First 1).Title
if ([string]::IsNullOrWhiteSpace($moduleTitle)) {
throw "Unable to detect module title in $Path"
}
$groupMap = [ordered]@{}
$currentCategory = $null
$currentSubcategory = $null
foreach ($section in $sections | Where-Object { $_.Level -ge 5 }) {
$title = $section.Title.Trim()
if ($title -in @("内容", "范例")) {
continue
}
$bodyLines = @()
if ($section.End -gt $section.Start) {
$bodyLines = $lines[($section.Start + 1)..$section.End]
}
$isFunction = Test-IsFunctionSection -Title $title -BodyLines $bodyLines
switch ($section.Level) {
5 {
if ($isFunction) {
Add-FunctionToGroup -GroupMap $groupMap -Category "直接函数" -Subcategory $null -FunctionName $title
}
else {
$currentCategory = $title
$currentSubcategory = $null
}
continue
}
6 {
if ($isFunction) {
Add-FunctionToGroup -GroupMap $groupMap -Category $currentCategory -Subcategory $null -FunctionName $title
}
else {
$currentSubcategory = $title
}
continue
}
7 {
if ($isFunction) {
Add-FunctionToGroup -GroupMap $groupMap -Category $currentCategory -Subcategory $currentSubcategory -FunctionName $title
}
continue
}
}
}
$groups = foreach ($entry in $groupMap.GetEnumerator()) {
[pscustomobject]@{
Title = $entry.Value.Title
Category = $entry.Value.Category
Subcategory = $entry.Value.Subcategory
Functions = @($entry.Value.Functions)
}
}
$functionCount = ($groups | ForEach-Object { $_.Functions } | Sort-Object -Unique).Count
return [pscustomobject]@{
File = [System.IO.Path]::GetFileNameWithoutExtension($Path)
ModuleTitle = $moduleTitle
Groups = $groups
FunctionCount = $functionCount
}
}
function New-MarkdownModulePage {
param(
[pscustomobject]$Module,
[string]$Summary
)
$lines = [System.Collections.Generic.List[string]]::new()
$lines.Add("# $($Module.ModuleTitle)")
$lines.Add("")
$lines.Add("这一页只负责函数定位:先按主题找到模块,再在页内搜索函数名。")
$lines.Add("")
$lines.Add("## 使用方式")
$lines.Add("")
$lines.Add("- 返回总目录:[catalog/index.md](index.md)")
$lines.Add("- 需要基础语法时回到 [../../syntax/index.md](../../syntax/index.md)")
$lines.Add("- 需要金融任务组织方式时回到 [../../finance/index.md](../../finance/index.md)")
$lines.Add("")
$lines.Add("## 模块范围")
$lines.Add("")
$lines.Add("- 说明:$Summary")
$lines.Add("- 主题数:$($Module.Groups.Count)")
$lines.Add("- 函数数:$($Module.FunctionCount)")
$lines.Add("")
$lines.Add("## 主题目录")
$lines.Add("")
foreach ($group in $Module.Groups) {
$lines.Add("### $($group.Title)")
$lines.Add("")
foreach ($functionName in $group.Functions) {
$lines.Add("- ``$functionName``")
}
$lines.Add("")
}
return ($lines -join "`r`n").TrimEnd() + "`r`n"
}
function New-MarkdownCatalogIndex {
param([object[]]$Modules)
$lines = [System.Collections.Generic.List[string]]::new()
$lines.Add("# Function Catalog")
$lines.Add("")
$lines.Add('这里是 canonical 函数目录。它只回答“函数在哪个模块里”,不承担基础语法教学。')
$lines.Add("")
$lines.Add("## 使用顺序")
$lines.Add("")
$lines.Add("1. 不知道函数在哪个模块,先看下面的模块目录。")
$lines.Add("2. 进入模块页后,在页内搜索具体函数名。")
$lines.Add("3. 如果问题是语法怎么写,回到 [../../syntax/index.md](../../syntax/index.md)。")
$lines.Add("4. 如果问题是金融场景如何组织,回到 [../../finance/index.md](../../finance/index.md)。")
$lines.Add("")
$lines.Add("## 模块目录")
$lines.Add("")
$lines.Add("| 模块 | 分类页 | 范围 | 函数数 |")
$lines.Add("| --- | --- | --- | --- |")
foreach ($module in $Modules) {
$pageName = "{0}.md" -f $module.File
$lines.Add("| $($module.ModuleTitle) | [$pageName]($pageName) | $($module.Summary) | $($module.FunctionCount) |")
}
$lines.Add("")
$lines.Add("## 说明")
$lines.Add("")
$lines.Add("- 这套目录页由仓库内的函数语料自动整理生成。")
$lines.Add("- 当前目标是先提供稳定检索层,再逐步补全更细的 canonical 说明。")
$lines.Add("")
return ($lines -join "`r`n").TrimEnd() + "`r`n"
}
if (-not (Test-Path -LiteralPath $sourceDir)) {
throw "Source directory not found: $sourceDir"
}
New-Item -ItemType Directory -Force -Path $outputDir | Out-Null
$modules = foreach ($meta in $moduleOrder) {
$sourcePath = Join-Path $sourceDir ("{0}.md" -f $meta.File)
if (-not (Test-Path -LiteralPath $sourcePath)) {
throw "Missing module source: $sourcePath"
}
$module = Parse-TslFunctionModule -Path $sourcePath
$module | Add-Member -NotePropertyName Summary -NotePropertyValue $meta.Summary
$module
}
$catalogIndex = New-MarkdownCatalogIndex -Modules $modules
Set-Content -LiteralPath (Join-Path $outputDir "index.md") -Value $catalogIndex -Encoding UTF8
foreach ($module in $modules) {
$page = New-MarkdownModulePage -Module $module -Summary $module.Summary
Set-Content -LiteralPath (Join-Path $outputDir ("{0}.md" -f $module.File)) -Value $page -Encoding UTF8
}
Write-Output ("Generated {0} catalog pages in {1}" -f ($modules.Count + 1), $outputDir)
+71
View File
@@ -0,0 +1,71 @@
param(
[string]$RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
)
$ErrorActionPreference = "Stop"
$targets = @(
(Join-Path $RepoRoot "docs/tsl/index.md"),
(Join-Path $RepoRoot "docs/tsl/syntax"),
(Join-Path $RepoRoot "docs/tsl/finance"),
(Join-Path $RepoRoot "docs/tsl/reference")
)
$excludeFragments = @(
"\docs\plans\",
"\archive\",
"\docs\tsl\legacy\",
"\docs\tsl\syntax_book\"
)
$pattern = '(?i)\b(?:syntax_book|legacy)\b'
function Get-RelativePath([string]$basePath, [string]$childPath) {
$baseUri = [System.Uri]::new(($basePath.TrimEnd('\', '/') + [System.IO.Path]::DirectorySeparatorChar))
$childUri = [System.Uri]::new($childPath)
return [System.Uri]::UnescapeDataString($baseUri.MakeRelativeUri($childUri).ToString()).Replace('\', '/')
}
$files = foreach ($target in $targets) {
if (!(Test-Path -LiteralPath $target)) {
throw "Target not found: $target"
}
if ((Get-Item -LiteralPath $target).PSIsContainer) {
Get-ChildItem -LiteralPath $target -Recurse -File -Filter *.md
}
else {
Get-Item -LiteralPath $target
}
}
$activeFiles = $files |
Where-Object {
$fullName = $_.FullName
foreach ($fragment in $excludeFragments) {
if ($fullName -like "*$fragment*") {
return $false
}
}
return $true
} |
Sort-Object FullName -Unique
$hits = foreach ($file in $activeFiles) {
Select-String -Path $file.FullName -Pattern $pattern -AllMatches
}
if ($hits.Count -eq 0) {
Write-Output "No legacy references found in active TSL docs."
exit 0
}
Write-Output "Legacy references found in active TSL docs:"
foreach ($hit in $hits) {
$relativePath = Get-RelativePath -basePath $RepoRoot -childPath $hit.Path
Write-Output ("{0}:{1}: {2}" -f $relativePath, $hit.LineNumber, $hit.Line.Trim())
}
Write-Output ("Total files scanned: {0}" -f $activeFiles.Count)
Write-Output ("Total legacy hits: {0}" -f $hits.Count)
exit 1
+174 -100
View File
@@ -1,4 +1,5 @@
#!/usr/bin/env python3
import re
import sys
from datetime import datetime, timezone
from pathlib import Path
@@ -21,7 +22,15 @@ ORDER = [
]
SCRIPT_DIR = Path(__file__).resolve().parent
PLAYBOOK_ROOT = SCRIPT_DIR.parent
PATH_CONFIG_KEYS = {"project_root", "target_dir", "agents_home", "codex_home"}
PATH_CONFIG_KEYS = {"project_root", "deploy_root", "agents_home", "codex_home"}
DOCS_INDEX_SECTION_HEADINGS = {
"common": "## 跨语言(common",
"tsl": "## TSLtsl/tsf",
"cpp": "## C++cpp",
"python": "## Pythonpython",
"typescript": "## TypeScripttypescript",
"markdown": "## Markdownmarkdown",
}
def usage() -> str:
@@ -232,6 +241,85 @@ def normalize_langs(raw: object) -> list[str]:
return cleaned
def normalize_relative_dir(raw: object, label: str) -> str:
value = str(raw).strip()
if not value:
raise ValueError(f"{label} is empty")
path = Path(value)
if path.is_absolute() or ".." in path.parts:
raise ValueError(f"invalid {label}: {value}")
normalized = path.as_posix()
return "." if normalized == "" else normalized
def join_deploy_subpath(root: str, child: str) -> str:
if root in ("", "."):
return child.lstrip("/")
return f"{root.rstrip('/')}/{child.lstrip('/')}"
def resolve_in_project_deploy_root(project_root: Path) -> str | None:
try:
rel = PLAYBOOK_ROOT.resolve().relative_to(project_root.resolve())
if str(rel) != ".":
return rel.as_posix()
except ValueError:
pass
return None
def config_requires_deploy_root(config: dict) -> bool:
for key in (
"vendor",
"sync_rules",
"sync_memory_bank",
"sync_prompts",
"sync_standards",
"install_skills",
):
if key in config:
return True
return False
def resolve_configured_deploy_root(config: dict, project_root: Path) -> str:
playbook_config = config.get("playbook", {})
raw = None
if isinstance(playbook_config, dict):
raw = playbook_config.get("deploy_root")
vendor_config = config.get("vendor", {})
if isinstance(vendor_config, dict) and vendor_config.get("target_dir") is not None:
raise ValueError(
"vendor.target_dir is no longer supported; use [playbook].deploy_root"
)
if raw is not None and str(raw).strip():
return normalize_relative_dir(raw, "deploy_root")
in_project_deploy_root = resolve_in_project_deploy_root(project_root)
if in_project_deploy_root is not None:
return in_project_deploy_root
if config_requires_deploy_root(config):
raise ValueError(
"playbook.deploy_root is required when running from an external clone; "
"set it to the target project's relative deployment path"
)
return "docs/standards/playbook"
def resolve_deploy_root(context: dict) -> str:
project_root: Path = context["project_root"]
in_project_deploy_root = resolve_in_project_deploy_root(project_root)
if in_project_deploy_root is not None:
return in_project_deploy_root
return context["deploy_root"]
def resolve_docs_prefix(context: dict) -> str:
return join_deploy_subpath(resolve_deploy_root(context), "docs")
def resolve_main_language(config: dict, context: dict) -> str:
raw = config.get("main_language")
if raw is not None and str(raw).strip():
@@ -254,21 +342,8 @@ def resolve_main_language(config: dict, context: dict) -> str:
def resolve_playbook_scripts(project_root: Path, context: dict) -> str:
playbook_scripts = PLAYBOOK_ROOT / "scripts"
try:
rel = playbook_scripts.resolve().relative_to(project_root.resolve())
return rel.as_posix()
except ValueError:
full_config = context.get("config", {})
if isinstance(full_config, dict):
vendor_conf = full_config.get("vendor")
if isinstance(vendor_conf, dict):
target_dir = vendor_conf.get("target_dir")
if target_dir:
target_str = str(target_dir).strip().rstrip("/").rstrip("\\")
if target_str:
return f"{target_str}/scripts"
return "docs/standards/playbook/scripts"
_ = project_root
return join_deploy_subpath(resolve_deploy_root(context), "scripts")
def read_git_commit(root: Path) -> str:
@@ -284,88 +359,75 @@ def read_git_commit(root: Path) -> str:
return result.stdout.strip() or "N/A"
def write_docs_index(dest_prefix: Path, langs: list[str]) -> None:
lines = [
"# 文档导航(Docs Index",
def extract_docs_index_sections(lines: list[str]) -> dict[str, list[str]]:
heading_to_key = {value: key for key, value in DOCS_INDEX_SECTION_HEADINGS.items()}
starts: list[tuple[int, str]] = []
for idx, line in enumerate(lines):
key = heading_to_key.get(line)
if key is not None:
starts.append((idx, key))
sections: dict[str, list[str]] = {}
for idx, (start, key) in enumerate(starts):
end = starts[idx + 1][0] if idx + 1 < len(starts) else len(lines)
section_lines = lines[start:end]
while section_lines and section_lines[-1] == "":
section_lines = section_lines[:-1]
sections[key] = section_lines
return sections
def build_docs_index_lines(langs: list[str], source_path: Path | None = None) -> list[str]:
docs_index_path = source_path or (PLAYBOOK_ROOT / "docs" / "index.md")
source_lines = docs_index_path.read_text(encoding="utf-8").splitlines()
title = source_lines[0] if source_lines else "# 文档导航(Docs Index"
sections = extract_docs_index_sections(source_lines)
ordered_keys = ["common", *langs]
result = [
title,
"",
f"本快照为裁剪版 Playbooklangs: {','.join(langs)})。",
"",
"## 跨语言(common",
"",
"- 提交信息与版本号:`common/commit_message.md`",
]
for lang in langs:
if lang == "tsl":
lines += [
"",
"## TSLtsl",
"",
"- 代码风格:`tsl/code_style.md`",
"- 命名规范:`tsl/naming.md`",
"- 语法手册:`tsl/syntax_book/index.md`",
"- 工具链与验证命令(模板):`tsl/toolchain.md`",
]
elif lang == "cpp":
lines += [
"",
"## C++cpp",
"",
"- 代码风格:`cpp/code_style.md`",
"- 命名规范:`cpp/naming.md`",
"- 工具链与验证命令(模板):`cpp/toolchain.md`",
"- 第三方依赖(Conan):`cpp/dependencies_conan.md`",
"- clangd 配置:`cpp/clangd.md`",
]
elif lang == "python":
lines += [
"",
"## Pythonpython",
"",
"- 代码风格:`python/style_guide.md`",
"- 工具链:`python/tooling.md`",
"- 配置清单:`python/configuration.md`",
]
elif lang == "typescript":
lines += [
"",
"## TypeScripttypescript",
"",
"- 代码风格:`typescript/code_style.md`",
"- 命名规范:`typescript/naming.md`",
"- 工具链:`typescript/toolchain.md`",
"- 配置清单:`typescript/configuration.md`",
]
elif lang == "markdown":
lines += [
"",
"## Markdownmarkdown",
"",
"- 代码块与行内代码格式:`markdown/index.md`",
]
for idx, key in enumerate(ordered_keys):
section = sections.get(key)
if section is None:
raise ValueError(f"docs/index.md is missing section for {key}")
if idx > 0:
result.append("")
result.extend(section)
return result
def write_docs_index(dest_prefix: Path, langs: list[str]) -> None:
lines = build_docs_index_lines(langs)
docs_index = dest_prefix / "docs/index.md"
ensure_dir(docs_index.parent)
docs_index.write_text("\n".join(lines) + "\n", encoding="utf-8")
def write_snapshot_readme(dest_prefix: Path, langs: list[str]) -> None:
def write_snapshot_readme(dest_prefix: Path, deploy_root: str, langs: list[str]) -> None:
scripts_path = join_deploy_subpath(deploy_root, "scripts/playbook.py")
docs_index_path = join_deploy_subpath(deploy_root, "docs/index.md")
lines = [
"# Playbook(裁剪快照)",
"",
f"本目录为从 Playbook vendoring 的裁剪快照(langs: {','.join(langs)})。",
f"本目录为从 Playbook 部署到项目内的裁剪快照(langs: {','.join(langs)})。",
"",
"## 使用",
"",
"在目标项目根目录执行:",
"",
"```sh",
"python docs/standards/playbook/scripts/playbook.py -config playbook.toml",
f"python {scripts_path} -config playbook.toml",
"```",
"",
"配置示例:`docs/standards/playbook/playbook.toml.example`",
f"配置示例:`{join_deploy_subpath(deploy_root, 'playbook.toml.example')}`",
"",
"文档入口:",
"",
"- `docs/standards/playbook/docs/index.md`",
f"- `{docs_index_path}`",
"- `.agents/index.md`",
]
(dest_prefix / "README.md").write_text("\n".join(lines) + "\n", encoding="utf-8")
@@ -393,11 +455,8 @@ def vendor_action(config: dict, context: dict) -> int:
print(f"ERROR: {exc}", file=sys.stderr)
return 2
target_dir = config.get("target_dir", "docs/standards/playbook")
target_path = Path(target_dir)
if target_path.is_absolute() or ".." in target_path.parts:
print(f"ERROR: invalid target_dir: {target_dir}", file=sys.stderr)
return 2
deploy_root = context["deploy_root"]
target_path = Path(deploy_root)
project_root: Path = context["project_root"]
dest_prefix = project_root / target_path
@@ -471,7 +530,7 @@ def vendor_action(config: dict, context: dict) -> int:
copy2(example_config, dest_prefix / "playbook.toml.example")
write_docs_index(dest_prefix, langs)
write_snapshot_readme(dest_prefix, langs)
write_snapshot_readme(dest_prefix, deploy_root, langs)
write_source_file(dest_prefix, langs)
log(f"Vendored snapshot -> {dest_prefix}")
@@ -910,32 +969,45 @@ def create_agents_index(agents_root: Path, langs: list[str], docs_prefix: str |
"",
"标准快照文档入口:",
"",
f"- {docs_prefix or 'docs/standards/playbook/docs/'}",
f"- {docs_prefix or 'docs/'}",
]
agents_index.write_text("\n".join(lines) + "\n", encoding="utf-8")
log("Synced .agents/index.md")
def rewrite_agents_docs_links(agents_dir: Path, docs_prefix: str) -> None:
def rewrite_docs_links_in_markdown(root: Path, docs_prefix: str, recursive: bool) -> None:
replacements = {
"`docs/tsl/": f"`{docs_prefix}/tsl/",
"`docs/cpp/": f"`{docs_prefix}/cpp/",
"`docs/python/": f"`{docs_prefix}/python/",
"`docs/typescript/": f"`{docs_prefix}/typescript/",
"`docs/markdown/": f"`{docs_prefix}/markdown/",
"`docs/common/": f"`{docs_prefix}/common/",
"tsl": f"{docs_prefix}/tsl/",
"cpp": f"{docs_prefix}/cpp/",
"python": f"{docs_prefix}/python/",
"typescript": f"{docs_prefix}/typescript/",
"markdown": f"{docs_prefix}/markdown/",
"common": f"{docs_prefix}/common/",
}
for md_path in agents_dir.glob("*.md"):
iterator = root.rglob("*.md") if recursive else root.glob("*.md")
patterns = [
(re.compile(rf"(?<![\w./-])docs/{section}/"), replacement)
for section, replacement in replacements.items()
]
for md_path in iterator:
if not md_path.is_file():
continue
text = md_path.read_text(encoding="utf-8")
updated = text
for old, new in replacements.items():
updated = updated.replace(old, new)
for pattern, replacement in patterns:
updated = pattern.sub(replacement, updated)
if updated != text:
md_path.write_text(updated, encoding="utf-8")
def rewrite_agents_docs_links(agents_dir: Path, docs_prefix: str) -> None:
rewrite_docs_links_in_markdown(agents_dir, docs_prefix, recursive=False)
def rewrite_skill_docs_links(skill_dir: Path, docs_prefix: str) -> None:
rewrite_docs_links_in_markdown(skill_dir, docs_prefix, recursive=True)
def read_gitattributes_entries(path: Path) -> list[str]:
entries: list[str] = []
for line in path.read_text(encoding="utf-8").splitlines():
@@ -1047,13 +1119,7 @@ def sync_standards_action(config: dict, context: dict) -> int:
copytree(src, dst)
log(f"Synced .agents/{lang} from standards.")
docs_prefix = None
try:
rel_snapshot = PLAYBOOK_ROOT.resolve().relative_to(project_root.resolve())
if str(rel_snapshot) != ".":
docs_prefix = f"{rel_snapshot.as_posix()}/docs"
except ValueError:
docs_prefix = None
docs_prefix = resolve_docs_prefix(context)
if docs_prefix:
for lang in langs:
@@ -1165,6 +1231,7 @@ def install_skills_action(config: dict, context: dict) -> int:
dst.rename(backup)
log(f"Backed up existing skill: {name} -> {backup.name}")
copytree(src, dst)
rewrite_skill_docs_links(dst, resolve_docs_prefix(context))
log(f"Installed: {name}")
return 0
@@ -1246,10 +1313,17 @@ def main(argv: list[str]) -> int:
root = (config_path.parent / root).resolve()
else:
root = config_path.parent
resolved_root = root.resolve()
try:
deploy_root = resolve_configured_deploy_root(config, resolved_root)
except ValueError as exc:
print(f"ERROR: {exc}", file=sys.stderr)
return 2
context = {
"project_root": root.resolve(),
"project_root": resolved_root,
"config_path": config_path.resolve(),
"config": config,
"deploy_root": deploy_root,
}
if should_sync_agents(config):
+328
View File
@@ -0,0 +1,328 @@
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$Path
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
function Resolve-AuditTargets {
param(
[string]$InputPath
)
if (-not (Test-Path -LiteralPath $InputPath)) {
throw "Path not found: $InputPath"
}
$item = Get-Item -LiteralPath $InputPath
if ($item.PSIsContainer) {
return @(Get-ChildItem -LiteralPath $item.FullName -Recurse -File -Filter "*.md" | Sort-Object FullName)
}
if ($item.Extension -ne ".md") {
throw "Only Markdown files are supported: $($item.FullName)"
}
return @($item)
}
function Get-FirstMeaningfulLine {
param(
[string[]]$Lines
)
foreach ($line in $Lines) {
if (-not [string]::IsNullOrWhiteSpace($line)) {
return $line.Trim()
}
}
return ""
}
function Get-BlockPreview {
param(
[string]$Code
)
$preview = ""
foreach ($line in ($Code -split "`r?`n")) {
if (-not [string]::IsNullOrWhiteSpace($line)) {
$preview = $line.Trim()
break
}
}
if ([string]::IsNullOrWhiteSpace($preview)) {
return "<empty>"
}
if ($preview.Length -gt 100) {
return $preview.Substring(0, 100) + "..."
}
return $preview
}
function Get-SkipReason {
param(
[string]$Language,
[string]$Code
)
if ($Language -eq "text") {
return "text block"
}
$trimmed = $Code.Trim()
if ([string]::IsNullOrWhiteSpace($trimmed)) {
return "empty block"
}
if ($trimmed -match '(?m)^\s*statement;\s*$') {
return "grammar placeholder"
}
if ($trimmed -match '…+' -or $trimmed -match '(?m)^\s*(//\s*)?\.{3,}\s*$') {
return "ellipsis placeholder"
}
if ($trimmed -match '<[^>\r\n]+>') {
return "angle-bracket placeholder"
}
$meaningfulLines = @(
($Code -split "`r?`n" | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | ForEach-Object { $_.Trim() })
)
if ($meaningfulLines.Count -gt 1 -and
$meaningfulLines[0] -eq "begin" -and
(($meaningfulLines | Select-Object -Skip 1) -match '^(?i:(function|unit|type|class|const|var|namespace|uses))')) {
return "mixed non-standalone snippet"
}
return $null
}
function Get-CompileKind {
param(
[string]$Code
)
$firstLine = Get-FirstMeaningfulLine -Lines ($Code -split "`r?`n")
if ($firstLine -match '^(?i:(function|unit|type|class|const|var|namespace|uses|\{\$))') {
return "tsf"
}
return "tsl"
}
function Invoke-TslCompile {
param(
[string]$Code,
[ValidateSet("tsl", "tsf")]
[string]$Kind
)
$tempRoot = Join-Path ([System.IO.Path]::GetTempPath()) ("tsl-doc-audit-" + [guid]::NewGuid().ToString("N"))
New-Item -ItemType Directory -Path $tempRoot | Out-Null
$process = $null
try {
$sourcePath = Join-Path $tempRoot ("snippet." + $Kind)
[System.IO.File]::WriteAllText($sourcePath, $Code, [System.Text.UTF8Encoding]::new($false))
$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = 'tsl'
$psi.Arguments = "-COMPILE `"$sourcePath`""
$psi.RedirectStandardOutput = $true
$psi.RedirectStandardError = $true
$psi.RedirectStandardInput = $true
$psi.UseShellExecute = $false
$psi.CreateNoWindow = $true
$process = New-Object System.Diagnostics.Process
$process.StartInfo = $psi
$process.Start() | Out-Null
$process.StandardInput.WriteLine('exit')
$process.StandardInput.Flush()
$process.WaitForExit(30000) | Out-Null
$stdout = $process.StandardOutput.ReadToEnd()
$stderr = $process.StandardError.ReadToEnd()
$outputText = ($stdout + "`n" + $stderr).Trim()
$lowerText = $outputText.ToLowerInvariant()
$success = $lowerText -match 'compile success'
$failure = $lowerText -match 'compile error'
if ($success) {
return [pscustomobject]@{ Success = $true; Output = $outputText }
}
if ($failure) {
return [pscustomobject]@{ Success = $false; Output = $outputText }
}
return [pscustomobject]@{ Success = $false; Output = (if ($outputText) { $outputText } else { "<no output>" }) }
}
finally {
if ($process -and -not $process.HasExited) {
$process.Kill()
$process.WaitForExit()
}
if (Test-Path -LiteralPath $tempRoot) {
Remove-Item -LiteralPath $tempRoot -Recurse -Force
}
}
}
function Wrap-AsFunctionBody {
param(
[string]$Code
)
$body = $Code -split "`r?`n" | ForEach-Object { " $_" }
return @(
"function __doc_check__();"
"begin"
$body
"end;"
""
) -join "`n"
}
function Parse-MarkdownBlocks {
param(
[string]$Content
)
$blocks = New-Object System.Collections.Generic.List[object]
$lines = $Content -split "`r?`n"
$inFence = $false
$fenceLang = ""
$fenceStartLine = 0
$buffer = New-Object System.Collections.Generic.List[string]
for ($i = 0; $i -lt $lines.Length; $i++) {
$line = $lines[$i]
if (-not $inFence) {
if ($line -match '^\s*```([A-Za-z0-9_-]*)\s*$') {
$inFence = $true
$fenceLang = $Matches[1].ToLowerInvariant()
$fenceStartLine = $i + 1
$buffer.Clear()
}
continue
}
if ($line -match '^\s*```\s*$') {
$blocks.Add([pscustomobject]@{
Language = $fenceLang
StartLine = $fenceStartLine
Code = ($buffer -join "`n")
})
$inFence = $false
$fenceLang = ""
$fenceStartLine = 0
$buffer.Clear()
continue
}
$buffer.Add($line)
}
return $blocks
}
$targets = Resolve-AuditTargets -InputPath $Path
$grandPass = 0
$grandSkip = 0
$grandFail = 0
foreach ($target in $targets) {
$content = Get-Content -LiteralPath $target.FullName -Raw
$blocks = Parse-MarkdownBlocks -Content $content
$results = New-Object System.Collections.Generic.List[object]
foreach ($block in $blocks) {
if ($block.Language -notin @("tsl", "text")) {
continue
}
$skipReason = Get-SkipReason -Language $block.Language -Code $block.Code
if ($null -ne $skipReason) {
$results.Add([pscustomobject]@{
Status = "skip"
StartLine = $block.StartLine
Preview = Get-BlockPreview -Code $block.Code
Detail = $skipReason
})
continue
}
$kind = Get-CompileKind -Code $block.Code
$compile = Invoke-TslCompile -Code $block.Code -Kind $kind
if ($compile.Success) {
$results.Add([pscustomobject]@{
Status = "pass"
StartLine = $block.StartLine
Preview = Get-BlockPreview -Code $block.Code
Detail = $kind
})
continue
}
if ($kind -eq "tsl") {
$wrappedCode = Wrap-AsFunctionBody -Code $block.Code
$wrappedCompile = Invoke-TslCompile -Code $wrappedCode -Kind "tsf"
if ($wrappedCompile.Success) {
$results.Add([pscustomobject]@{
Status = "pass"
StartLine = $block.StartLine
Preview = Get-BlockPreview -Code $block.Code
Detail = "wrapped tsf"
})
continue
}
$compile = $wrappedCompile
}
$results.Add([pscustomobject]@{
Status = "fail"
StartLine = $block.StartLine
Preview = Get-BlockPreview -Code $block.Code
Detail = ($compile.Output.Trim())
})
}
$passCount = @($results | Where-Object Status -eq "pass").Count
$skipCount = @($results | Where-Object Status -eq "skip").Count
$failCount = @($results | Where-Object Status -eq "fail").Count
$grandPass += $passCount
$grandSkip += $skipCount
$grandFail += $failCount
Write-Output "$($target.FullName): pass=$passCount skip=$skipCount fail=$failCount"
foreach ($failure in ($results | Where-Object Status -eq "fail")) {
Write-Output " FAIL line $($failure.StartLine): $($failure.Preview)"
foreach ($detailLine in ($failure.Detail -split "`r?`n")) {
if (-not [string]::IsNullOrWhiteSpace($detailLine)) {
Write-Output " $detailLine"
}
}
}
}
Write-Output "TOTAL: pass=$grandPass skip=$grandSkip fail=$grandFail"
if ($grandFail -gt 0) {
exit 1
}
exit 0