📝 docs(tsl): rebuild canonical syntax and routing manual
This commit is contained in:
@@ -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)
|
||||
@@ -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
@@ -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": "## TSL(tsl/tsf)",
|
||||
"cpp": "## C++(cpp)",
|
||||
"python": "## Python(python)",
|
||||
"typescript": "## TypeScript(typescript)",
|
||||
"markdown": "## Markdown(markdown)",
|
||||
}
|
||||
|
||||
|
||||
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"本快照为裁剪版 Playbook(langs: {','.join(langs)})。",
|
||||
"",
|
||||
"## 跨语言(common)",
|
||||
"",
|
||||
"- 提交信息与版本号:`common/commit_message.md`",
|
||||
]
|
||||
for lang in langs:
|
||||
if lang == "tsl":
|
||||
lines += [
|
||||
"",
|
||||
"## TSL(tsl)",
|
||||
"",
|
||||
"- 代码风格:`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 += [
|
||||
"",
|
||||
"## Python(python)",
|
||||
"",
|
||||
"- 代码风格:`python/style_guide.md`",
|
||||
"- 工具链:`python/tooling.md`",
|
||||
"- 配置清单:`python/configuration.md`",
|
||||
]
|
||||
elif lang == "typescript":
|
||||
lines += [
|
||||
"",
|
||||
"## TypeScript(typescript)",
|
||||
"",
|
||||
"- 代码风格:`typescript/code_style.md`",
|
||||
"- 命名规范:`typescript/naming.md`",
|
||||
"- 工具链:`typescript/toolchain.md`",
|
||||
"- 配置清单:`typescript/configuration.md`",
|
||||
]
|
||||
elif lang == "markdown":
|
||||
lines += [
|
||||
"",
|
||||
"## Markdown(markdown)",
|
||||
"",
|
||||
"- 代码块与行内代码格式:`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):
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user