skill-family-doc-render 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/README.md +106 -20
- package/package.json +1 -1
- package/schemas/public-release.schema.json +30 -0
- package/schemas/site-coverage-lock.schema.json +28 -0
- package/scripts/render-public-site.mjs +343 -15
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# 变更日志
|
|
2
2
|
|
|
3
|
+
## 0.3.0 - 2026-09-07
|
|
4
|
+
|
|
5
|
+
### 新增
|
|
6
|
+
|
|
7
|
+
- 增加 `site.coverage` 和最小 `site-coverage-lock.json`,记录已审阅产品输入的文件摘要、
|
|
8
|
+
聚合摘要,以及启用 artifact-graph 时的版本锁摘要。
|
|
9
|
+
- 增加只读的 `--status --repo <name>` 和只更新覆盖快照的
|
|
10
|
+
`--refresh-coverage --repo <name>`。状态命令会报告覆盖提交、当前摘要、变化文件、
|
|
11
|
+
权威版本值和建议执行的 Git diff 命令。
|
|
12
|
+
- 状态命令从 `repo.source` 发现所属 Git 仓库。目标位于外层工作区中的独立内层仓库时,
|
|
13
|
+
覆盖提交、变化路径和 diff 建议只使用内层仓库历史。
|
|
14
|
+
- 增加 `site.versionSources`。渲染器从收容于 `repo.source` 的 JSON 文件和 JSON Pointer
|
|
15
|
+
读取版本值,版本源文件自动加入覆盖输入。
|
|
16
|
+
|
|
17
|
+
### 安全与兼容
|
|
18
|
+
|
|
19
|
+
- 覆盖输入拒绝绝对路径、越界路径、空匹配和符号链接逃逸;覆盖锁始终排除自身,
|
|
20
|
+
避免形成摘要循环。
|
|
21
|
+
- 保留现有静态 token 和按仓库名派生版本 token 的兼容行为;同名
|
|
22
|
+
`site.versionSources` 取值优先。
|
|
23
|
+
|
|
3
24
|
## 0.2.1 - 2026-09-06
|
|
4
25
|
|
|
5
26
|
### 变更
|
package/README.md
CHANGED
|
@@ -13,15 +13,21 @@ publication APIs. A failure before commit removes staging and leaves the existin
|
|
|
13
13
|
untouched. After commit, or when publication is indeterminate, staging or displaced data may
|
|
14
14
|
remain for diagnosis; the renderer does not guess or roll back.
|
|
15
15
|
|
|
16
|
+
> **Development status:** this worktree is the unpublished `0.3.0` candidate. The latest npm
|
|
17
|
+
> release is `0.2.1`; it does not contain `site.coverage`, `site.versionSources`, `--status`, or
|
|
18
|
+
> `--refresh-coverage`.
|
|
19
|
+
|
|
16
20
|
## Install
|
|
17
21
|
|
|
22
|
+
Install the current published release with an exact version:
|
|
23
|
+
|
|
18
24
|
```sh
|
|
19
|
-
npm install skill-family-doc-render@0.2.
|
|
25
|
+
npm install --save-exact skill-family-doc-render@0.2.1
|
|
20
26
|
```
|
|
21
27
|
|
|
22
28
|
Requires Node.js `>=22.22.2 <23` (aligned with the skill-family foundation packages).
|
|
23
29
|
|
|
24
|
-
0.
|
|
30
|
+
The unpublished 0.3.0 candidate consumes the three Foundation packages at the exact `0.18.0` pin:
|
|
25
31
|
`skill-family-contracts@0.18.0`, `skill-family-harness-node@0.18.0` and
|
|
26
32
|
`skill-family-engineering-kit@0.18.0` (the last one is used by the workspace profile check).
|
|
27
33
|
The 0.2.0 baseline format and exit-code changes are breaking changes; existing 0.1.x users
|
|
@@ -30,24 +36,47 @@ notes.
|
|
|
30
36
|
|
|
31
37
|
## CLI
|
|
32
38
|
|
|
39
|
+
The published 0.2.1 release supports rendering, drift checks, repository selection, and the Git
|
|
40
|
+
index assertion:
|
|
41
|
+
|
|
33
42
|
```sh
|
|
34
43
|
# render all repos with a site field (writes to disk)
|
|
35
|
-
npx skill-family-doc-render@0.2.
|
|
44
|
+
npx skill-family-doc-render@0.2.1
|
|
36
45
|
|
|
37
46
|
# drift check only: compare in-memory render against committed site-baseline.json, write nothing
|
|
38
|
-
npx skill-family-doc-render@0.2.
|
|
47
|
+
npx skill-family-doc-render@0.2.1 --check
|
|
39
48
|
|
|
40
49
|
# render / check a single repo by name
|
|
41
|
-
npx skill-family-doc-render@0.2.
|
|
50
|
+
npx skill-family-doc-render@0.2.1 --repo <name>
|
|
42
51
|
|
|
43
52
|
# additionally assert every rendered file is tracked in the git index
|
|
44
|
-
npx skill-family-doc-render@0.2.
|
|
53
|
+
npx skill-family-doc-render@0.2.1 --assert-git
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Run the unpublished 0.3.0 coverage commands from a local package checkout. The current directory
|
|
57
|
+
must still be the workspace that owns `public-release.json`; adjust the script path when the
|
|
58
|
+
package is nested in a monorepo:
|
|
59
|
+
|
|
60
|
+
```sh
|
|
61
|
+
# inspect whether reviewed product inputs have changed; writes nothing
|
|
62
|
+
node bin/skill-family-doc-render.mjs --status --repo <name>
|
|
63
|
+
|
|
64
|
+
# refresh only the reviewed-input snapshot after semantic and style review
|
|
65
|
+
node bin/skill-family-doc-render.mjs --refresh-coverage --repo <name>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
After 0.3.0 is published, consumers can install or invoke that exact release:
|
|
69
|
+
|
|
70
|
+
```sh
|
|
71
|
+
npm install --save-exact skill-family-doc-render@0.3.0
|
|
72
|
+
npx skill-family-doc-render@0.3.0 --status --repo <name>
|
|
45
73
|
```
|
|
46
74
|
|
|
47
75
|
Exit codes: `0` success; `1` drift class — `--check` drift, leak-scan hit, `--assert-git`
|
|
48
|
-
missing files, missing/corrupt baseline; `2`
|
|
49
|
-
`public-release.json` or `pages.json`, JSON parse
|
|
50
|
-
invalid `--repo` usage
|
|
76
|
+
missing files, missing/corrupt render baseline, or missing/stale coverage snapshot; `2`
|
|
77
|
+
configuration/tool class — missing/invalid `public-release.json` or `pages.json`, JSON parse
|
|
78
|
+
failures, unreplaced `@TOKEN@` placeholders, invalid `--repo` usage, invalid coverage inputs, or
|
|
79
|
+
Git failure (the message includes the JSON field path). When several repos are
|
|
51
80
|
rendered in one run, each repo is isolated: a failure in one does not stop the others, and the
|
|
52
81
|
run ends with a summary of successes/failures plus a non-zero exit (2 if any failure was
|
|
53
82
|
configuration-class, otherwise 1).
|
|
@@ -56,7 +85,8 @@ configuration-class, otherwise 1).
|
|
|
56
85
|
|
|
57
86
|
The config file lives at the workspace root (resolved from `process.cwd()`). It is validated
|
|
58
87
|
against `schemas/public-release.schema.json` (JSON Schema 2020-12, strict policy) before
|
|
59
|
-
anything runs.
|
|
88
|
+
anything runs. `coverage` and `versionSources` in the example below belong to the unpublished
|
|
89
|
+
0.3.0 candidate.
|
|
60
90
|
|
|
61
91
|
```json
|
|
62
92
|
{
|
|
@@ -72,6 +102,21 @@ anything runs.
|
|
|
72
102
|
"dir": "site-src",
|
|
73
103
|
"target": "docs",
|
|
74
104
|
"pages": "pages.json",
|
|
105
|
+
"coverage": {
|
|
106
|
+
"lock": "site-coverage-lock.json",
|
|
107
|
+
"inputs": ["package.json", "src/**", "schemas/**"]
|
|
108
|
+
},
|
|
109
|
+
"versionSources": {
|
|
110
|
+
"@COMPONENT_VERSION@": {
|
|
111
|
+
"source": "package.json",
|
|
112
|
+
"pointer": "/version"
|
|
113
|
+
},
|
|
114
|
+
"@COMPONENT_TAG@": {
|
|
115
|
+
"source": "package.json",
|
|
116
|
+
"pointer": "/version",
|
|
117
|
+
"prefix": "component-v"
|
|
118
|
+
}
|
|
119
|
+
},
|
|
75
120
|
"tokens": { "@CUSTOM_NOTE@": "any static placeholder" }
|
|
76
121
|
}
|
|
77
122
|
}
|
|
@@ -88,6 +133,13 @@ anything runs.
|
|
|
88
133
|
`inPager: false` excludes a page from the prev/next pager. Page `id` is restricted to
|
|
89
134
|
`[A-Za-z0-9][A-Za-z0-9_-]*`.
|
|
90
135
|
- `forbiddenPublicPaths` / `privateLiterals` append project-specific literals to the leak scan.
|
|
136
|
+
- `site.coverage` declares reviewed product inputs relative to `repo.source`. Each input accepts
|
|
137
|
+
`*` and `**` glob syntax and must match at least one file after the coverage lock itself is
|
|
138
|
+
excluded.
|
|
139
|
+
- `site.versionSources` maps a token to an existing JSON file and an
|
|
140
|
+
[RFC 6901 JSON Pointer](https://www.rfc-editor.org/rfc/rfc6901). `prefix` and `suffix` format
|
|
141
|
+
the scalar value without copying the version into configuration. Every referenced JSON file
|
|
142
|
+
automatically joins the coverage inputs.
|
|
91
143
|
|
|
92
144
|
## Placeholders
|
|
93
145
|
|
|
@@ -103,7 +155,8 @@ Version tokens are derived automatically from the repo's `package.json` version
|
|
|
103
155
|
- `@{NAME}_TAG@` — e.g. `my-project` → `@MY_PROJECT_TAG@` = `tagPrefix` + version
|
|
104
156
|
- `@{NAME}_VERSION@` — the bare version
|
|
105
157
|
|
|
106
|
-
`site.tokens` adds
|
|
158
|
+
`site.tokens` adds non-version static placeholder replacements. A token also declared in
|
|
159
|
+
`site.versionSources` always takes its value from the JSON source. If a rendered page still
|
|
107
160
|
contains an unreplaced `@TOKEN@` (`@[A-Z0-9_]+@`), the run fails fast (exit 2) naming the
|
|
108
161
|
tokens and the file — placeholder typos never slip into the published site silently.
|
|
109
162
|
|
|
@@ -111,6 +164,29 @@ If a repo has no `package.json` (or no valid `version`), the version falls back
|
|
|
111
164
|
with a loud `WARNING` on stderr, because that fallback is baked into the output and the
|
|
112
165
|
baseline; a malformed `package.json` fails fast (exit 2).
|
|
113
166
|
|
|
167
|
+
## Coverage status
|
|
168
|
+
|
|
169
|
+
`--refresh-coverage --repo <name>` expands `site.coverage.inputs`, adds every configured version
|
|
170
|
+
source, computes the file digests and closure digest, then atomically writes the declared coverage
|
|
171
|
+
lock. It does not render the site or modify Git. The lock contains only:
|
|
172
|
+
|
|
173
|
+
- `sha256`: the Foundation resource-closure digest;
|
|
174
|
+
- `inputs`: sorted relative file paths and their byte digests;
|
|
175
|
+
- `artifactGraphVersionLockSha256`: the current `artifacts/traceability-version-lock.json`
|
|
176
|
+
digest when the repository contains `artifact-graph.config.yaml`.
|
|
177
|
+
|
|
178
|
+
`--status --repo <name>` recomputes the same facts without writing. It prints the last commit that
|
|
179
|
+
changed the coverage lock, the current and recorded closure digests, changed covered files,
|
|
180
|
+
current values from `site.versionSources`, and suggested `git diff` commands. A matching snapshot
|
|
181
|
+
returns `0`; a missing or stale snapshot returns `1`. A site without `coverage`, a path escape, an
|
|
182
|
+
empty glob, an invalid JSON pointer, a symlink escape, or a Git failure returns `2`.
|
|
183
|
+
|
|
184
|
+
Coverage paths are relative to `repo.source`. Absolute paths, lexical traversal, realpath escape,
|
|
185
|
+
and symlink escape are rejected through Foundation path containment. The coverage lock is always
|
|
186
|
+
excluded from its own inputs, avoiding a digest cycle. `--status` requires a Git repository with a
|
|
187
|
+
resolvable `HEAD`, because its report includes committed, staged, unstaged, and covered untracked
|
|
188
|
+
changes.
|
|
189
|
+
|
|
114
190
|
## Leak scan
|
|
115
191
|
|
|
116
192
|
Every output file — rendered pages **and** copied assets (`.html`/`.css`/`.js`/`.svg`) — is
|
|
@@ -151,30 +227,35 @@ dependencies:
|
|
|
151
227
|
- [`skill-family-harness-node`](https://github.com/ifoohoo/skill-family-harness-node) — all file
|
|
152
228
|
reads go through `resolveContained` / `readFileContained` (path traversal, symlink and realpath
|
|
153
229
|
escapes rejected), per-file digests use `digestBytes`, the tree baseline is computed with
|
|
154
|
-
`computeResourceClosure`. Output is staged in a
|
|
155
|
-
parent, with contained writes through
|
|
230
|
+
`computeResourceClosure`, and coverage locks use `writeFileAtomic`. Output is staged in a
|
|
231
|
+
sibling directory under the target's canonical parent, with contained writes through
|
|
232
|
+
`writeFileAtomic`. A new target uses
|
|
156
233
|
`createFixedSetPublicationManifest` followed by `publishFixedSet`; an existing target uses
|
|
157
234
|
`replaceFixedSetAtomic`. Staging is removed after success or a clearly pre-commit failure;
|
|
158
235
|
post-commit or indeterminate failures may retain it for diagnosis. These APIs do not add a
|
|
159
236
|
broader atomicity guarantee than Foundation provides.
|
|
160
237
|
- [`skill-family-contracts`](https://github.com/ifoohoo/skill-family-contracts) —
|
|
161
238
|
`validateDocument` (JSON Schema 2020-12, strict policy) validates `public-release.json`,
|
|
162
|
-
`pages.json` and `site-
|
|
239
|
+
`pages.json`, `site-baseline.json`, and `site-coverage-lock.json`; validation failure aborts
|
|
240
|
+
with exit code 2.
|
|
163
241
|
|
|
164
242
|
## License
|
|
165
243
|
|
|
166
244
|
Apache-2.0
|
|
167
245
|
|
|
168
246
|
<!-- release-skill:capability:safe-first-command -->
|
|
169
|
-
> **Start here:**
|
|
170
|
-
>
|
|
247
|
+
> **Start here:** for the unpublished 0.3.0 candidate, run
|
|
248
|
+
> `node bin/skill-family-doc-render.mjs --check` from its local checkout. Published 0.2.1 users
|
|
249
|
+
> can run `./node_modules/.bin/skill-family-doc-render --check`. The drift check is read-only:
|
|
171
250
|
> it renders in memory, compares against the committed `site-baseline.json`, writes
|
|
172
251
|
> nothing and touches no network or credentials. The CLI performs no Git writes; only an
|
|
173
252
|
> explicit `--assert-git` performs a read-only query of the Git index.
|
|
174
253
|
|
|
175
254
|
<!-- release-skill:capability:external-write-boundary -->
|
|
176
|
-
> **External write boundary:**
|
|
177
|
-
> staging directory, both within the target's canonical parent, with contained path checks.
|
|
255
|
+
> **External write boundary:** rendering writes the declared target and a controlled sibling
|
|
256
|
+
> staging directory, both within the target's canonical parent, with contained path checks. An
|
|
257
|
+
> explicit `--refresh-coverage` writes only the declared coverage lock through an atomic file
|
|
258
|
+
> replacement. `--status` and `--check` write nothing.
|
|
178
259
|
> Render, scan, baseline and `--assert-git` preflight failures happen before target changes.
|
|
179
260
|
> Staging is removed after success or a clearly pre-commit failure; post-commit or
|
|
180
261
|
> indeterminate failures may retain it to preserve diagnosable state.
|
|
@@ -184,7 +265,12 @@ Apache-2.0
|
|
|
184
265
|
|
|
185
266
|
## Minimal example
|
|
186
267
|
|
|
268
|
+
Use the local checkout while 0.3.0 remains unpublished:
|
|
269
|
+
|
|
187
270
|
```sh
|
|
188
|
-
|
|
189
|
-
|
|
271
|
+
node bin/skill-family-doc-render.mjs --help
|
|
272
|
+
node bin/skill-family-doc-render.mjs --check
|
|
190
273
|
```
|
|
274
|
+
|
|
275
|
+
Replace `node bin/skill-family-doc-render.mjs` with `npx skill-family-doc-render@0.3.0` only after
|
|
276
|
+
the registry publishes 0.3.0.
|
package/package.json
CHANGED
|
@@ -29,6 +29,36 @@
|
|
|
29
29
|
"dir": { "type": "string", "minLength": 1 },
|
|
30
30
|
"target": { "type": "string", "minLength": 1 },
|
|
31
31
|
"pages": { "type": "string", "minLength": 1 },
|
|
32
|
+
"coverage": {
|
|
33
|
+
"type": "object",
|
|
34
|
+
"additionalProperties": false,
|
|
35
|
+
"required": ["lock", "inputs"],
|
|
36
|
+
"properties": {
|
|
37
|
+
"lock": { "type": "string", "minLength": 1, "pattern": "^[^*?\\[\\]{}()!]+$" },
|
|
38
|
+
"inputs": {
|
|
39
|
+
"type": "array",
|
|
40
|
+
"minItems": 1,
|
|
41
|
+
"uniqueItems": true,
|
|
42
|
+
"items": { "type": "string", "minLength": 1, "pattern": "^[^?\\[\\]{}()!]+$" }
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"versionSources": {
|
|
47
|
+
"type": "object",
|
|
48
|
+
"minProperties": 1,
|
|
49
|
+
"propertyNames": { "pattern": "^@[A-Z0-9_]+@$" },
|
|
50
|
+
"additionalProperties": {
|
|
51
|
+
"type": "object",
|
|
52
|
+
"additionalProperties": false,
|
|
53
|
+
"required": ["source", "pointer"],
|
|
54
|
+
"properties": {
|
|
55
|
+
"source": { "type": "string", "minLength": 1, "pattern": "^[^*?\\[\\]{}()!]+$" },
|
|
56
|
+
"pointer": { "type": "string", "pattern": "^/" },
|
|
57
|
+
"prefix": { "type": "string" },
|
|
58
|
+
"suffix": { "type": "string" }
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
},
|
|
32
62
|
"tokens": {
|
|
33
63
|
"type": "object",
|
|
34
64
|
"additionalProperties": {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$id": "https://github.com/ifoohoo/skill-family-doc-render/schemas/site-coverage-lock.schema.json",
|
|
3
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4
|
+
"title": "site-coverage-lock.json",
|
|
5
|
+
"description": "知识站已审阅产品输入的最小字节快照;不保存版本、发布状态或人工结论。",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"required": ["sha256", "inputs"],
|
|
9
|
+
"properties": {
|
|
10
|
+
"sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" },
|
|
11
|
+
"inputs": {
|
|
12
|
+
"type": "array",
|
|
13
|
+
"items": {
|
|
14
|
+
"type": "object",
|
|
15
|
+
"additionalProperties": false,
|
|
16
|
+
"required": ["path", "sha256"],
|
|
17
|
+
"properties": {
|
|
18
|
+
"path": { "type": "string", "minLength": 1 },
|
|
19
|
+
"sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" }
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"artifactGraphVersionLockSha256": {
|
|
24
|
+
"type": "string",
|
|
25
|
+
"pattern": "^[0-9a-f]{64}$"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -14,23 +14,24 @@
|
|
|
14
14
|
// harness 化:所有配置 / 源文件读取经 readFileContained,staging 的相对输出经
|
|
15
15
|
// writeFileAtomic 收容并原子写入(见 skill-family-harness-node);树基线摘要用
|
|
16
16
|
// computeResourceClosure,逐文件摘要用 digestBytes。
|
|
17
|
-
// 配置、pages.json
|
|
17
|
+
// 配置、pages.json、渲染基线与覆盖快照先经 skill-family-contracts validateDocument
|
|
18
18
|
// (JSON Schema 2020-12,strict 策略)校验,校验失败 fail-fast。
|
|
19
19
|
//
|
|
20
20
|
// 错误分类(退出码语义):
|
|
21
|
-
// exit 1 — 漂移类:--check
|
|
22
|
-
//
|
|
23
|
-
// exit 2 —
|
|
24
|
-
//
|
|
21
|
+
// exit 1 — 漂移类:--check 检出漂移、覆盖快照缺失或落后、泄漏扫描命中、
|
|
22
|
+
// --assert-git 缺失、渲染基线缺失 / 基线 JSON 损坏;
|
|
23
|
+
// exit 2 — 配置或工具类:配置 / JSON / 覆盖输入非法、Git 调用失败、
|
|
24
|
+
// @TOKEN@ 占位符残留、--repo 参数无效;
|
|
25
25
|
// 其余意外异常(HarnessError 等)原样抛出,CLI 归为 exit 1。
|
|
26
26
|
// 可预期失败统一封装为 RenderError 携带 exitCode。
|
|
27
27
|
import { existsSync, readFileSync } from 'node:fs';
|
|
28
|
-
import { readdir, rm, mkdtemp, realpath } from 'node:fs/promises';
|
|
29
|
-
import { resolve, relative, extname, join, dirname, basename } from 'node:path';
|
|
28
|
+
import { readdir, rm, mkdtemp, realpath, stat, glob } from 'node:fs/promises';
|
|
29
|
+
import { resolve, relative, extname, join, dirname, basename, posix } from 'node:path';
|
|
30
30
|
import { fileURLToPath } from 'node:url';
|
|
31
31
|
import { execFileSync } from 'node:child_process';
|
|
32
32
|
import {
|
|
33
33
|
resolveContained,
|
|
34
|
+
classifyPathInput,
|
|
34
35
|
readFileContained,
|
|
35
36
|
digestBytes,
|
|
36
37
|
computeResourceClosure,
|
|
@@ -51,14 +52,17 @@ const publicReleaseSchema = JSON.parse(
|
|
|
51
52
|
const siteBaselineSchema = JSON.parse(
|
|
52
53
|
readFileSync(join(__dirname, '..', 'schemas', 'site-baseline.schema.json'), 'utf8'),
|
|
53
54
|
);
|
|
55
|
+
const siteCoverageLockSchema = JSON.parse(
|
|
56
|
+
readFileSync(join(__dirname, '..', 'schemas', 'site-coverage-lock.schema.json'), 'utf8'),
|
|
57
|
+
);
|
|
54
58
|
const pagesSchema = JSON.parse(
|
|
55
59
|
readFileSync(join(__dirname, '..', 'schemas', 'pages.schema.json'), 'utf8'),
|
|
56
60
|
);
|
|
57
61
|
|
|
58
62
|
// 可预期失败(配置缺失、校验失败、check 漂移、泄漏命中等)以此类型抛出,携带退出码。
|
|
59
63
|
export class RenderError extends Error {
|
|
60
|
-
constructor(message, exitCode = 2) {
|
|
61
|
-
super(message);
|
|
64
|
+
constructor(message, exitCode = 2, cause) {
|
|
65
|
+
super(message, cause === undefined ? undefined : { cause });
|
|
62
66
|
this.exitCode = exitCode;
|
|
63
67
|
}
|
|
64
68
|
}
|
|
@@ -105,6 +109,301 @@ async function pkgVersion(root, source, name) {
|
|
|
105
109
|
return doc.version;
|
|
106
110
|
}
|
|
107
111
|
|
|
112
|
+
function configError(name, message, cause) {
|
|
113
|
+
return new RenderError(`[render-public-site] ${name}: ${message}`, 2, cause);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function repoSourceRoot(root, repo) {
|
|
117
|
+
const classification = classifyPathInput(repo.source);
|
|
118
|
+
if (!classification.ok) {
|
|
119
|
+
throw configError(repo.name, `repo.source 非法 (${classification.kind}): ${repo.source}`);
|
|
120
|
+
}
|
|
121
|
+
if (resolve(root, repo.source) === root) return root;
|
|
122
|
+
try {
|
|
123
|
+
return await resolveContained(root, repo.source);
|
|
124
|
+
} catch (cause) {
|
|
125
|
+
throw configError(repo.name, `repo.source 未收容于工作区: ${repo.source}`, cause);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function containedOrConfig(root, relPath, repoName, fieldPath) {
|
|
130
|
+
try {
|
|
131
|
+
return await resolveContained(root, relPath);
|
|
132
|
+
} catch (cause) {
|
|
133
|
+
throw configError(repoName, `${fieldPath} 必须是收容于 repo.source 的相对路径: ${relPath}`, cause);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function jsonPointerValue(document, pointer, repoName, fieldPath) {
|
|
138
|
+
const segments = pointer.slice(1).split('/').map((segment) => {
|
|
139
|
+
if (/~(?:[^01]|$)/.test(segment)) {
|
|
140
|
+
throw configError(repoName, `${fieldPath} 不是有效 JSON Pointer: ${pointer}`);
|
|
141
|
+
}
|
|
142
|
+
return segment.replaceAll('~1', '/').replaceAll('~0', '~');
|
|
143
|
+
});
|
|
144
|
+
let value = document;
|
|
145
|
+
for (const segment of segments) {
|
|
146
|
+
if (value === null || typeof value !== 'object' || !Object.hasOwn(value, segment)) {
|
|
147
|
+
throw configError(repoName, `${fieldPath} 指向的字段不存在: ${pointer}`);
|
|
148
|
+
}
|
|
149
|
+
value = value[segment];
|
|
150
|
+
}
|
|
151
|
+
if (!['string', 'number', 'boolean'].includes(typeof value)) {
|
|
152
|
+
throw configError(repoName, `${fieldPath} 必须指向 string、number 或 boolean: ${pointer}`);
|
|
153
|
+
}
|
|
154
|
+
return value;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function loadVersionSources(sourceRoot, repo) {
|
|
158
|
+
const definitions = repo.site.versionSources || {};
|
|
159
|
+
const cache = new Map();
|
|
160
|
+
const tokens = {};
|
|
161
|
+
const values = [];
|
|
162
|
+
const paths = new Set();
|
|
163
|
+
for (const [token, definition] of Object.entries(definitions)) {
|
|
164
|
+
const fieldPath = `site.versionSources.${token}`;
|
|
165
|
+
const sourceAbs = await containedOrConfig(sourceRoot, definition.source, repo.name, `${fieldPath}.source`);
|
|
166
|
+
let sourceStats;
|
|
167
|
+
try {
|
|
168
|
+
sourceStats = await stat(sourceAbs);
|
|
169
|
+
} catch (cause) {
|
|
170
|
+
throw configError(repo.name, `${fieldPath}.source 不可读取: ${definition.source}`, cause);
|
|
171
|
+
}
|
|
172
|
+
if (!sourceStats.isFile()) {
|
|
173
|
+
throw configError(repo.name, `${fieldPath}.source 必须指向 JSON 文件: ${definition.source}`);
|
|
174
|
+
}
|
|
175
|
+
let document = cache.get(definition.source);
|
|
176
|
+
if (document === undefined) {
|
|
177
|
+
try {
|
|
178
|
+
document = JSON.parse(await readFileContained(sourceRoot, definition.source, { encoding: 'utf8' }));
|
|
179
|
+
} catch (cause) {
|
|
180
|
+
if (cause instanceof SyntaxError) {
|
|
181
|
+
throw configError(repo.name, `${fieldPath}.source JSON 解析失败: ${definition.source}: ${cause.message}`, cause);
|
|
182
|
+
}
|
|
183
|
+
throw configError(repo.name, `${fieldPath}.source 读取失败: ${definition.source}`, cause);
|
|
184
|
+
}
|
|
185
|
+
cache.set(definition.source, document);
|
|
186
|
+
}
|
|
187
|
+
const rawValue = jsonPointerValue(document, definition.pointer, repo.name, `${fieldPath}.pointer`);
|
|
188
|
+
const renderedValue = `${definition.prefix || ''}${rawValue}${definition.suffix || ''}`;
|
|
189
|
+
tokens[token] = renderedValue;
|
|
190
|
+
values.push({ token, source: definition.source, pointer: definition.pointer, value: rawValue, renderedValue });
|
|
191
|
+
paths.add(posix.normalize(definition.source.replaceAll('\\', '/')));
|
|
192
|
+
}
|
|
193
|
+
return { tokens, values, paths: [...paths].sort() };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function artifactGraphLockDigest(sourceRoot, repo) {
|
|
197
|
+
const configPath = 'artifact-graph.config.yaml';
|
|
198
|
+
const lockPath = 'artifacts/traceability-version-lock.json';
|
|
199
|
+
const configAbs = await containedOrConfig(sourceRoot, configPath, repo.name, 'artifact-graph config');
|
|
200
|
+
if (!existsSync(configAbs)) return undefined;
|
|
201
|
+
const lockAbs = await containedOrConfig(sourceRoot, lockPath, repo.name, 'artifact-graph version lock');
|
|
202
|
+
if (!existsSync(lockAbs)) {
|
|
203
|
+
throw configError(repo.name, `已启用 artifact-graph,但未找到 ${lockPath}`);
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
return digestBytes(await readFileContained(sourceRoot, lockPath));
|
|
207
|
+
} catch (cause) {
|
|
208
|
+
throw configError(repo.name, `${lockPath} 读取失败`, cause);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function computeCoverageSnapshot(sourceRoot, repo) {
|
|
213
|
+
const coverage = repo.site.coverage;
|
|
214
|
+
if (!coverage) throw configError(repo.name, 'site.coverage 未配置');
|
|
215
|
+
const lockPath = posix.normalize(coverage.lock.replaceAll('\\', '/'));
|
|
216
|
+
await containedOrConfig(sourceRoot, coverage.lock, repo.name, 'site.coverage.lock');
|
|
217
|
+
const versionSources = await loadVersionSources(sourceRoot, repo);
|
|
218
|
+
if (versionSources.paths.includes(lockPath)) {
|
|
219
|
+
throw configError(repo.name, 'site.coverage.lock 不能同时作为版本源');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const files = new Set();
|
|
223
|
+
for (const [index, pattern] of coverage.inputs.entries()) {
|
|
224
|
+
const fieldPath = `site.coverage.inputs[${index}]`;
|
|
225
|
+
if (pattern.split('/').includes('..')) {
|
|
226
|
+
throw configError(repo.name, `${fieldPath} 不能包含 .. 路径段: ${pattern}`);
|
|
227
|
+
}
|
|
228
|
+
// 首版只接受 * / **,避免 brace、extglob 或字符类先展开再越过收容检查。
|
|
229
|
+
if (/[?\[\]{}()!]/.test(pattern)) {
|
|
230
|
+
throw configError(repo.name, `${fieldPath} 只支持 * 与 ** 通配符: ${pattern}`);
|
|
231
|
+
}
|
|
232
|
+
await containedOrConfig(sourceRoot, pattern.replaceAll('*', '__coverage_glob__'), repo.name, fieldPath);
|
|
233
|
+
const matches = [];
|
|
234
|
+
try {
|
|
235
|
+
for await (const candidate of glob(pattern, { cwd: sourceRoot })) {
|
|
236
|
+
const normalized = posix.normalize(candidate.replaceAll('\\', '/'));
|
|
237
|
+
if (normalized === '.' || normalized === lockPath) continue;
|
|
238
|
+
const candidateAbs = await containedOrConfig(
|
|
239
|
+
sourceRoot,
|
|
240
|
+
normalized,
|
|
241
|
+
repo.name,
|
|
242
|
+
fieldPath,
|
|
243
|
+
);
|
|
244
|
+
const candidateStats = await stat(candidateAbs);
|
|
245
|
+
if (candidateStats.isFile()) matches.push(normalized);
|
|
246
|
+
}
|
|
247
|
+
} catch (cause) {
|
|
248
|
+
if (cause instanceof RenderError) throw cause;
|
|
249
|
+
throw configError(repo.name, `${fieldPath} 无法展开: ${pattern}`, cause);
|
|
250
|
+
}
|
|
251
|
+
if (!matches.length) {
|
|
252
|
+
throw configError(repo.name, `${fieldPath} 没有匹配任何输入文件: ${pattern}`);
|
|
253
|
+
}
|
|
254
|
+
for (const match of matches) files.add(match);
|
|
255
|
+
}
|
|
256
|
+
for (const versionPath of versionSources.paths) {
|
|
257
|
+
await containedOrConfig(sourceRoot, versionPath, repo.name, 'site.versionSources.source');
|
|
258
|
+
files.add(versionPath);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
let closure;
|
|
262
|
+
try {
|
|
263
|
+
closure = await computeResourceClosure({
|
|
264
|
+
root: sourceRoot,
|
|
265
|
+
resources: [...files].sort().map((path) => ({ path, role: 'input' })),
|
|
266
|
+
});
|
|
267
|
+
} catch (cause) {
|
|
268
|
+
throw configError(repo.name, '覆盖输入摘要计算失败', cause);
|
|
269
|
+
}
|
|
270
|
+
const snapshot = {
|
|
271
|
+
sha256: closure.digest,
|
|
272
|
+
inputs: closure.resources.map(({ path, sha256 }) => ({ path, sha256 })),
|
|
273
|
+
};
|
|
274
|
+
const versionLockSha256 = await artifactGraphLockDigest(sourceRoot, repo);
|
|
275
|
+
if (versionLockSha256 !== undefined) {
|
|
276
|
+
snapshot.artifactGraphVersionLockSha256 = versionLockSha256;
|
|
277
|
+
}
|
|
278
|
+
assertValid(snapshot, siteCoverageLockSchema, `${repo.name} site-coverage-lock.json`);
|
|
279
|
+
return { snapshot, versionSources };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function git(root, args, repoName) {
|
|
283
|
+
try {
|
|
284
|
+
return execFileSync('git', args, { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
|
|
285
|
+
} catch (cause) {
|
|
286
|
+
const detail = String(cause.stderr || cause.message).trim();
|
|
287
|
+
throw configError(repoName, `Git 命令失败: git ${args.join(' ')}${detail ? `: ${detail}` : ''}`, cause);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function splitLines(text) {
|
|
292
|
+
return text ? text.split(/\r?\n/).filter(Boolean) : [];
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function shellQuote(value) {
|
|
296
|
+
return `'${String(value).replaceAll("'", "'\\''")}'`;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function coverageGitStatus(sourceRoot, repo, snapshot, baseline) {
|
|
300
|
+
const gitRoot = git(sourceRoot, ['rev-parse', '--show-toplevel'], repo.name);
|
|
301
|
+
const head = git(sourceRoot, ['rev-parse', 'HEAD'], repo.name);
|
|
302
|
+
const lockAbs = resolve(sourceRoot, repo.site.coverage.lock);
|
|
303
|
+
const lockGitPath = relative(gitRoot, lockAbs).replaceAll('\\', '/');
|
|
304
|
+
const coverageCommit = git(sourceRoot, ['log', '-1', '--format=%H', '--', lockGitPath], repo.name) || null;
|
|
305
|
+
const sourceGitPath = relative(gitRoot, sourceRoot).replaceAll('\\', '/') || '.';
|
|
306
|
+
const knownInputs = new Set([...(baseline?.inputs || []), ...snapshot.inputs].map((entry) => {
|
|
307
|
+
return relative(gitRoot, resolve(sourceRoot, entry.path)).replaceAll('\\', '/');
|
|
308
|
+
}));
|
|
309
|
+
if (snapshot.artifactGraphVersionLockSha256 || baseline?.artifactGraphVersionLockSha256) {
|
|
310
|
+
knownInputs.add(
|
|
311
|
+
relative(gitRoot, resolve(sourceRoot, 'artifacts/traceability-version-lock.json')).replaceAll('\\', '/'),
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
const changed = new Set();
|
|
315
|
+
const collect = (lines) => {
|
|
316
|
+
for (const path of lines) if (knownInputs.has(path)) changed.add(path);
|
|
317
|
+
};
|
|
318
|
+
if (coverageCommit) {
|
|
319
|
+
collect(splitLines(git(sourceRoot, ['diff', '--name-only', '--no-renames', `${coverageCommit}..${head}`], repo.name)));
|
|
320
|
+
} else {
|
|
321
|
+
for (const path of knownInputs) changed.add(path);
|
|
322
|
+
}
|
|
323
|
+
collect(splitLines(git(sourceRoot, ['diff', '--cached', '--name-only', '--no-renames'], repo.name)));
|
|
324
|
+
collect(splitLines(git(sourceRoot, ['diff', '--name-only', '--no-renames'], repo.name)));
|
|
325
|
+
collect(splitLines(git(sourceRoot, ['ls-files', '--others', '--exclude-standard'], repo.name)));
|
|
326
|
+
|
|
327
|
+
const suggestions = coverageCommit
|
|
328
|
+
? [
|
|
329
|
+
`git diff ${coverageCommit}..HEAD -- ${shellQuote(sourceGitPath)}`,
|
|
330
|
+
`git diff --cached -- ${shellQuote(sourceGitPath)}`,
|
|
331
|
+
`git diff -- ${shellQuote(sourceGitPath)}`,
|
|
332
|
+
]
|
|
333
|
+
: [
|
|
334
|
+
`git status --short -- ${shellQuote(sourceGitPath)}`,
|
|
335
|
+
`git diff HEAD -- ${shellQuote(sourceGitPath)}`,
|
|
336
|
+
];
|
|
337
|
+
return { coverageCommit, changed: [...changed].sort(), suggestions };
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
async function readCoverageBaseline(sourceRoot, repo) {
|
|
341
|
+
const lockPath = repo.site.coverage.lock;
|
|
342
|
+
const lockAbs = await containedOrConfig(sourceRoot, lockPath, repo.name, 'site.coverage.lock');
|
|
343
|
+
if (!existsSync(lockAbs)) return null;
|
|
344
|
+
let baseline;
|
|
345
|
+
try {
|
|
346
|
+
baseline = JSON.parse(await readFileContained(sourceRoot, lockPath, { encoding: 'utf8' }));
|
|
347
|
+
} catch (cause) {
|
|
348
|
+
throw configError(repo.name, `覆盖快照 JSON 解析失败: ${lockPath}: ${cause.message}`, cause);
|
|
349
|
+
}
|
|
350
|
+
assertValid(baseline, siteCoverageLockSchema, `${repo.name} site-coverage-lock.json`);
|
|
351
|
+
return baseline;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
async function refreshCoverageLock(root, repo) {
|
|
355
|
+
const sourceRoot = await repoSourceRoot(root, repo);
|
|
356
|
+
// 已存在的写入目标必须已经是覆盖快照,避免错误配置把普通项目文件当作锁覆盖。
|
|
357
|
+
await readCoverageBaseline(sourceRoot, repo);
|
|
358
|
+
const { snapshot } = await computeCoverageSnapshot(sourceRoot, repo);
|
|
359
|
+
try {
|
|
360
|
+
await writeFileAtomic(sourceRoot, repo.site.coverage.lock, `${JSON.stringify(snapshot, null, 2)}\n`);
|
|
361
|
+
} catch (cause) {
|
|
362
|
+
throw configError(repo.name, `覆盖快照写入失败: ${repo.site.coverage.lock}`, cause);
|
|
363
|
+
}
|
|
364
|
+
console.log(`[render-public-site --refresh-coverage] ${repo.name}: 已写入 ${repo.site.coverage.lock}`);
|
|
365
|
+
console.log(`[render-public-site --refresh-coverage] ${repo.name}: coverage-sha256=${snapshot.sha256}`);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
async function reportCoverageStatus(root, repo) {
|
|
369
|
+
if (!repo.site.coverage) throw configError(repo.name, 'site.coverage 未配置');
|
|
370
|
+
const sourceRoot = await repoSourceRoot(root, repo);
|
|
371
|
+
const baseline = await readCoverageBaseline(sourceRoot, repo);
|
|
372
|
+
const { snapshot, versionSources } = await computeCoverageSnapshot(sourceRoot, repo);
|
|
373
|
+
const gitStatus = coverageGitStatus(sourceRoot, repo, snapshot, baseline);
|
|
374
|
+
const consistent = baseline !== null
|
|
375
|
+
&& baseline.sha256 === snapshot.sha256
|
|
376
|
+
&& JSON.stringify(baseline.inputs) === JSON.stringify(snapshot.inputs)
|
|
377
|
+
&& baseline.artifactGraphVersionLockSha256 === snapshot.artifactGraphVersionLockSha256;
|
|
378
|
+
const state = baseline === null ? '缺失' : consistent ? '一致' : '落后';
|
|
379
|
+
console.log(`[render-public-site --status] ${repo.name}: ${state}`);
|
|
380
|
+
console.log(` 覆盖提交: ${gitStatus.coverageCommit || '(覆盖快照尚未提交)'}`);
|
|
381
|
+
console.log(` 当前输入摘要: ${snapshot.sha256}`);
|
|
382
|
+
console.log(` 快照输入摘要: ${baseline?.sha256 || '(缺失)'}`);
|
|
383
|
+
if (snapshot.artifactGraphVersionLockSha256 !== undefined) {
|
|
384
|
+
console.log(` artifact-graph 版本锁摘要: ${snapshot.artifactGraphVersionLockSha256}`);
|
|
385
|
+
}
|
|
386
|
+
console.log(' 变化文件:');
|
|
387
|
+
if (gitStatus.changed.length) {
|
|
388
|
+
for (const path of gitStatus.changed) console.log(` - ${path}`);
|
|
389
|
+
} else {
|
|
390
|
+
console.log(' - (无)');
|
|
391
|
+
}
|
|
392
|
+
console.log(' 版本源当前值:');
|
|
393
|
+
if (versionSources.values.length) {
|
|
394
|
+
for (const item of versionSources.values) {
|
|
395
|
+
console.log(` - ${item.token}=${item.renderedValue} (${item.source}#${item.pointer})`);
|
|
396
|
+
}
|
|
397
|
+
} else {
|
|
398
|
+
console.log(' - (未配置)');
|
|
399
|
+
}
|
|
400
|
+
console.log(' 建议 git diff:');
|
|
401
|
+
for (const command of gitStatus.suggestions) console.log(` ${command}`);
|
|
402
|
+
if (!consistent) {
|
|
403
|
+
throw new RenderError(`[render-public-site --status] ${repo.name}: 覆盖快照${baseline === null ? '缺失' : '落后'}`, 1);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
108
407
|
// --- 树摘要(基于 computeResourceClosure,排除 site-baseline.json 自身,防 hash 循环) ---
|
|
109
408
|
async function treeDigest(rootAbs, files) {
|
|
110
409
|
const closure = await computeResourceClosure({
|
|
@@ -183,13 +482,15 @@ async function renderRepo(root, repo, release, leakLiterals, { check, assertGit
|
|
|
183
482
|
return `<footer class="site">\n <div>© ${owner} · <a href="https://www.apache.org/licenses/LICENSE-2.0" rel="noopener noreferrer">Apache-2.0</a></div>\n <div class="generated">generated by skill-family-doc-render</div>\n</footer>`;
|
|
184
483
|
}
|
|
185
484
|
|
|
186
|
-
//
|
|
485
|
+
// 版本占位符:保留按 repo 名自动派生的兼容 token;显式 JSON 版本源最后覆盖静态 token。
|
|
187
486
|
const version = await pkgVersion(root, repo.source, repo.name);
|
|
487
|
+
const versionSources = await loadVersionSources(sourceAbs, repo);
|
|
188
488
|
const tn = tokenName(repo.name);
|
|
189
489
|
const tokens = {
|
|
190
490
|
[`@${tn}_TAG@`]: (repo.tagPrefix || `${repo.name}-v`) + version,
|
|
191
491
|
[`@${tn}_VERSION@`]: version,
|
|
192
492
|
...(site.tokens || {}),
|
|
493
|
+
...versionSources.tokens,
|
|
193
494
|
};
|
|
194
495
|
function replaceTokens(s) {
|
|
195
496
|
let out = s;
|
|
@@ -353,7 +654,8 @@ async function renderRepo(root, repo, release, leakLiterals, { check, assertGit
|
|
|
353
654
|
}
|
|
354
655
|
|
|
355
656
|
// === 主流程 ===
|
|
356
|
-
// 闭集参数解析:允许参数只有 --check / --assert-git / --
|
|
657
|
+
// 闭集参数解析:允许参数只有 --check / --assert-git / --status /
|
|
658
|
+
// --refresh-coverage / --repo <name> / --help / -h。
|
|
357
659
|
// 未知选项、游离位置参数、重复取值参数、--repo 缺值或值本身是另一个选项,
|
|
358
660
|
// 均以 RenderError(exit 2)失败,错误信息指出具体参数。不引入 CLI 框架。
|
|
359
661
|
// --help / -h 由 main() 统一呈现:闭集解析先于配置读取与写盘,打印 USAGE_TEXT 后退出 0。
|
|
@@ -364,11 +666,22 @@ Usage:
|
|
|
364
666
|
skill-family-doc-render --check read-only drift check against site-baseline.json
|
|
365
667
|
skill-family-doc-render --repo <name> render / check a single repo by name
|
|
366
668
|
skill-family-doc-render --assert-git also assert rendered files are git-tracked
|
|
669
|
+
skill-family-doc-render --status --repo <name>
|
|
670
|
+
read-only coverage status and Git changes
|
|
671
|
+
skill-family-doc-render --refresh-coverage --repo <name>
|
|
672
|
+
refresh only site-coverage-lock.json
|
|
367
673
|
|
|
368
674
|
Reads public-release.json from the current working directory.`;
|
|
369
675
|
|
|
370
676
|
function parseArgs(argv) {
|
|
371
|
-
const flags = {
|
|
677
|
+
const flags = {
|
|
678
|
+
check: false,
|
|
679
|
+
assertGit: false,
|
|
680
|
+
status: false,
|
|
681
|
+
refreshCoverage: false,
|
|
682
|
+
repoArg: null,
|
|
683
|
+
help: false,
|
|
684
|
+
};
|
|
372
685
|
let i = 0;
|
|
373
686
|
while (i < argv.length) {
|
|
374
687
|
const arg = argv[i];
|
|
@@ -376,6 +689,10 @@ function parseArgs(argv) {
|
|
|
376
689
|
flags.check = true;
|
|
377
690
|
} else if (arg === '--assert-git') {
|
|
378
691
|
flags.assertGit = true;
|
|
692
|
+
} else if (arg === '--status') {
|
|
693
|
+
flags.status = true;
|
|
694
|
+
} else if (arg === '--refresh-coverage') {
|
|
695
|
+
flags.refreshCoverage = true;
|
|
379
696
|
} else if (arg === '--help' || arg === '-h') {
|
|
380
697
|
flags.help = true;
|
|
381
698
|
} else if (arg === '--repo') {
|
|
@@ -395,6 +712,15 @@ function parseArgs(argv) {
|
|
|
395
712
|
}
|
|
396
713
|
i += 1;
|
|
397
714
|
}
|
|
715
|
+
if (flags.status && flags.refreshCoverage) {
|
|
716
|
+
throw new RenderError('[render-public-site] 参数错误: --status 与 --refresh-coverage 不能同时使用', 2);
|
|
717
|
+
}
|
|
718
|
+
if ((flags.status || flags.refreshCoverage) && (flags.check || flags.assertGit)) {
|
|
719
|
+
throw new RenderError('[render-public-site] 参数错误: 覆盖命令不能与 --check 或 --assert-git 组合', 2);
|
|
720
|
+
}
|
|
721
|
+
if ((flags.status || flags.refreshCoverage) && flags.repoArg === null) {
|
|
722
|
+
throw new RenderError('[render-public-site] 参数错误: --status 与 --refresh-coverage 必须配合 --repo <name>', 2);
|
|
723
|
+
}
|
|
398
724
|
return flags;
|
|
399
725
|
}
|
|
400
726
|
|
|
@@ -404,7 +730,7 @@ function parseArgs(argv) {
|
|
|
404
730
|
// 单个 repo 内部(含泄漏扫描命中)保持 fail-fast。
|
|
405
731
|
export async function main(argv = process.argv.slice(2), { cwd = process.cwd() } = {}) {
|
|
406
732
|
// 闭集解析在读取任何配置或写盘之前完成;参数失败时目标树一个字节都不动。
|
|
407
|
-
const { check, assertGit, repoArg, help } = parseArgs(argv);
|
|
733
|
+
const { check, assertGit, status, refreshCoverage, repoArg, help } = parseArgs(argv);
|
|
408
734
|
if (help) {
|
|
409
735
|
console.log(USAGE_TEXT);
|
|
410
736
|
return 0;
|
|
@@ -442,13 +768,15 @@ export async function main(argv = process.argv.slice(2), { cwd = process.cwd() }
|
|
|
442
768
|
const results = [];
|
|
443
769
|
for (const repo of selected) {
|
|
444
770
|
try {
|
|
445
|
-
await
|
|
771
|
+
if (status) await reportCoverageStatus(root, repo);
|
|
772
|
+
else if (refreshCoverage) await refreshCoverageLock(root, repo);
|
|
773
|
+
else await renderRepo(root, repo, release, leakLiterals, { check, assertGit });
|
|
446
774
|
results.push({ name: repo.name, ok: true });
|
|
447
775
|
} catch (err) {
|
|
448
776
|
results.push({
|
|
449
777
|
name: repo.name,
|
|
450
778
|
ok: false,
|
|
451
|
-
exitCode: err
|
|
779
|
+
exitCode: Number.isInteger(err?.exitCode) ? err.exitCode : 1,
|
|
452
780
|
message: err.message,
|
|
453
781
|
cause: err,
|
|
454
782
|
});
|