skill-family-doc-render 0.4.5 → 0.5.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 +18 -0
- package/README.md +69 -30
- package/bin/skill-family-doc-render.mjs +2 -1
- package/package.json +3 -3
- package/scripts/professional-conclusion.mjs +360 -0
- package/scripts/render-public-site.mjs +341 -34
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# 变更日志
|
|
2
2
|
|
|
3
|
+
## 0.5.0 - 2026-09-28
|
|
4
|
+
|
|
5
|
+
### 新增
|
|
6
|
+
|
|
7
|
+
- `--check-project` 可以附带 `--conclusion-output <绝对路径>`。机械检查结束后,CLI 排他创建一份共同证明,不覆盖已有文件,也不改站点。
|
|
8
|
+
- 新增 `--read-proof --proof-root <root> --proof <相对路径>`。它读取一份本族旧证明并打印 JSON,不重新扫描目标,也不要求网站配置。该模式与 `--check-project` 互斥。
|
|
9
|
+
|
|
10
|
+
### 变更
|
|
11
|
+
|
|
12
|
+
- 渲染器、文档插件和公开站点的本地源版本对齐为 0.5.0,作为计划中的联合发布目标。安装或 `npx` 前,先在官方 npm 确认 `skill-family-doc-render@0.5.0` 可用。
|
|
13
|
+
- 已发布的 0.4.5 没有 `--conclusion-output` 和 `--read-proof`。本版出证和读证示例使用精确的 `npx skill-family-doc-render@0.5.0`。
|
|
14
|
+
|
|
15
|
+
### 说明
|
|
16
|
+
|
|
17
|
+
- 当前源码的出证和读证已在本地候选上验证。可引用的记录仍是当时的直接 CLI 与四场 Codex 代表场景,覆盖限制保持原记录。负向场景只说明当时如实停止。
|
|
18
|
+
- 旧证明只记录原目标当时的结论。机械检查通过只说明机械范围,链接通过不代替中文语义审阅。记载未通过、未完成或未能开展检查的证明,结论保持为未通过。
|
|
19
|
+
- 0.5.0 的发布、Hub 登记和四个宿主的新回复验收,以各自的官方记录为准。历史五类宿主的入口调用仍限于当时范围。
|
|
20
|
+
|
|
3
21
|
## 0.4.5 - 2026-09-10
|
|
4
22
|
|
|
5
23
|
### 变更
|
package/README.md
CHANGED
|
@@ -16,64 +16,99 @@ publication APIs. A failure before commit removes staging and leaves the existin
|
|
|
16
16
|
untouched. After commit, or when publication is indeterminate, staging or displaced data may
|
|
17
17
|
remain for diagnosis; the renderer does not guess or roll back.
|
|
18
18
|
|
|
19
|
-
> **Version boundary:** this source tree is version `0.
|
|
20
|
-
>
|
|
21
|
-
> `
|
|
22
|
-
>
|
|
19
|
+
> **Version boundary:** this source tree is version `0.5.0`. That is the local version and the
|
|
20
|
+
> intended joint target for the renderer, documentation plugin, and public site. Confirm this
|
|
21
|
+
> exact version in the official npm registry before `npm install` or `npx`. Published `0.4.5`
|
|
22
|
+
> preserves the `markdown-v1`, `editorial`, and `--check-project` behavior introduced in 0.4.0
|
|
23
|
+
> and has no `--conclusion-output` or `--read-proof`. Those two commands belong to this 0.5.0
|
|
24
|
+
> CLI. The examples give the command shape for that version. They leave publication, Skill
|
|
25
|
+
> Family Hub registration, and new host-reply acceptance to the official records for `0.5.0`.
|
|
23
26
|
|
|
24
27
|
## Install
|
|
25
28
|
|
|
26
|
-
After confirming
|
|
29
|
+
After confirming `skill-family-doc-render@0.5.0` in the official npm registry, install that exact version:
|
|
27
30
|
|
|
28
31
|
```sh
|
|
29
|
-
npm install --save-exact skill-family-doc-render@0.
|
|
32
|
+
npm install --save-exact skill-family-doc-render@0.5.0
|
|
30
33
|
```
|
|
31
34
|
|
|
32
35
|
Requires Node.js `>=22.22.2 <23` (aligned with the skill-family foundation packages).
|
|
33
36
|
|
|
34
|
-
|
|
35
|
-
`skill-family-contracts@0.
|
|
37
|
+
This checkout currently pins exact runtime dependencies on `marked@18.0.11`,
|
|
38
|
+
`skill-family-contracts@0.22.0`, and `skill-family-harness-node@0.22.0`. `marked` supplies the
|
|
36
39
|
Markdown lexer; the renderer applies its own restricted-format validation before passing semantic
|
|
37
40
|
content to the template. The workspace profile check separately uses
|
|
38
|
-
`skill-family-engineering-kit@0.
|
|
41
|
+
`skill-family-engineering-kit@0.22.0`. These pins describe the local source and installed
|
|
42
|
+
packages; they do not republish `0.5.0`.
|
|
39
43
|
The 0.2.0 baseline format and exit-code changes are breaking changes; existing 0.1.x users
|
|
40
44
|
must re-render their baseline before using `--check`. See the changelog for the full migration
|
|
41
45
|
notes.
|
|
42
46
|
|
|
43
47
|
## CLI
|
|
44
48
|
|
|
45
|
-
Use the exact 0.
|
|
49
|
+
Use the exact 0.5.0 CLI for rendering, checks, coverage maintenance, the Git index assertion, and the proof commands. Confirm `skill-family-doc-render@0.5.0` in the official npm registry before running them. Published 0.4.5 can run the commands that omit `--conclusion-output` and `--read-proof`.
|
|
46
50
|
|
|
47
51
|
```sh
|
|
48
52
|
# render all repos with a site field (writes to disk)
|
|
49
|
-
npx skill-family-doc-render@0.
|
|
53
|
+
npx skill-family-doc-render@0.5.0
|
|
50
54
|
|
|
51
55
|
# drift check only: compare in-memory render against the on-disk site-baseline.json, write nothing
|
|
52
|
-
npx skill-family-doc-render@0.
|
|
56
|
+
npx skill-family-doc-render@0.5.0 --check
|
|
53
57
|
|
|
54
58
|
# render / check a single repo by name
|
|
55
|
-
npx skill-family-doc-render@0.
|
|
59
|
+
npx skill-family-doc-render@0.5.0 --repo <name>
|
|
56
60
|
|
|
57
61
|
# assert every rendered file is tracked in the Git index, then render
|
|
58
|
-
npx skill-family-doc-render@0.
|
|
62
|
+
npx skill-family-doc-render@0.5.0 --assert-git
|
|
59
63
|
|
|
60
64
|
# run both the drift and Git index tracking checks without writing
|
|
61
|
-
npx skill-family-doc-render@0.
|
|
65
|
+
npx skill-family-doc-render@0.5.0 --check --assert-git
|
|
62
66
|
|
|
63
67
|
# inspect or refresh the reviewed product-input snapshot
|
|
64
|
-
npx skill-family-doc-render@0.
|
|
65
|
-
npx skill-family-doc-render@0.
|
|
66
|
-
npx skill-family-doc-render@0.
|
|
68
|
+
npx skill-family-doc-render@0.5.0 --status --repo <name>
|
|
69
|
+
npx skill-family-doc-render@0.5.0 --refresh-coverage --repo <name>
|
|
70
|
+
npx skill-family-doc-render@0.5.0 --check-project --repo <name>
|
|
71
|
+
|
|
72
|
+
# after the mechanical check, exclusive-create one proof; do not overwrite
|
|
73
|
+
npx skill-family-doc-render@0.5.0 --check-project --repo <name> --conclusion-output /absolute/path/conclusion.json
|
|
74
|
+
|
|
75
|
+
# read one existing proof from any working directory; write nothing
|
|
76
|
+
npx skill-family-doc-render@0.5.0 --read-proof --proof-root /absolute/proof-root --proof relative/conclusion.json
|
|
67
77
|
```
|
|
68
78
|
|
|
69
|
-
|
|
70
|
-
|
|
79
|
+
`--check-project` still requires the current directory to own `public-release.json`.
|
|
80
|
+
`--read-proof` does not read that file and does not require a site configuration.
|
|
71
81
|
|
|
72
82
|
`--check-project` requires exactly one `--repo <name>`. It cannot be combined with `--check`,
|
|
73
|
-
`--status`, `--refresh-coverage`, `--assert-git`, or `--
|
|
74
|
-
safety, the mechanical Markdown contract, coverage freshness, the in-memory render
|
|
75
|
-
then internal links and resources. It
|
|
76
|
-
|
|
83
|
+
`--status`, `--refresh-coverage`, `--assert-git`, `--help`, or `--read-proof`. It checks input and
|
|
84
|
+
public-content safety, the mechanical Markdown contract, coverage freshness, the in-memory render
|
|
85
|
+
and baseline, then internal links and resources. It does not re-render to make the check pass, and
|
|
86
|
+
it does not run target scripts or hooks. Without `--conclusion-output` it writes nothing. A failure
|
|
87
|
+
identifies the repo, `public-release.json`, the reason, and a copyable
|
|
88
|
+
`skill-family-docs-render-site` recovery prompt.
|
|
89
|
+
|
|
90
|
+
`--conclusion-output` is optional and only valid with `--check-project`. The path must be
|
|
91
|
+
absolute. When present, the renderer exclusive-writes one Foundation
|
|
92
|
+
`professional-conclusion` file after the mechanical check. It does not replace an existing file.
|
|
93
|
+
The conclusion records only checks that actually ran. Domain codes are `DOCS_CLEAR`,
|
|
94
|
+
`DOCS_FINDINGS`, `DOCS_CHECK_INCOMPLETE`, and `DOCS_CHECK_UNAVAILABLE`. The renderer conclusion is
|
|
95
|
+
mechanical only; a passing link check is not a Chinese semantic review. A stored proof records
|
|
96
|
+
the original target's conclusion at that time.
|
|
97
|
+
|
|
98
|
+
`--read-proof` is mutually exclusive with `--check-project`. It reads one proof from
|
|
99
|
+
`--proof-root` + `--proof`, validates the common structure, and prints a JSON result with
|
|
100
|
+
`status` (`pass`, `not_pass`, or `unavailable`), `reason`, `provider`, and `conclusion`.
|
|
101
|
+
It does not rescan the project, follow paths inside the proof, or execute the target.
|
|
102
|
+
Same-version `DOCS_CLEAR` maps to `pass` only when `completion` is `complete`;
|
|
103
|
+
`DOCS_FINDINGS`, `DOCS_CHECK_INCOMPLETE`, and `DOCS_CHECK_UNAVAILABLE` map to `not_pass`.
|
|
104
|
+
That status keeps the proof's recorded non-pass. A missing file, damaged JSON, a different
|
|
105
|
+
provider, or an unknown same-version domain code is `unavailable` (exit `2`). Earlier
|
|
106
|
+
local-candidate runs exercised proof output and proof reading; those runs keep their original
|
|
107
|
+
scene limits. Publication, Hub registration, and new host replies for `0.5.0` stay with the
|
|
108
|
+
official records. These proof commands consume the Foundation
|
|
109
|
+
`professional-conclusion` contract from `skill-family-contracts@0.22.0`. Refreshing a proof
|
|
110
|
+
means running `--check-project` again to a new output path. Remediation, site creation, and
|
|
111
|
+
render writes stay on their original authorized commands.
|
|
77
112
|
|
|
78
113
|
Exit codes: `0` success; `1` drift/status class — `--check` drift, leak-scan hit, `--assert-git`
|
|
79
114
|
untracked rendered files, missing/corrupt render baseline, or missing/stale coverage snapshot; `2`
|
|
@@ -283,9 +318,11 @@ Foundation packages below, it uses exact `marked@18.0.11` for Markdown tokenizat
|
|
|
283
318
|
Apache-2.0
|
|
284
319
|
|
|
285
320
|
<!-- release-skill:capability:safe-first-command -->
|
|
286
|
-
> **Start here:** run `npx skill-family-doc-render@0.
|
|
287
|
-
> confirming that exact
|
|
288
|
-
> `
|
|
321
|
+
> **Start here:** run `npx skill-family-doc-render@0.5.0 --check-project --repo <name>` after
|
|
322
|
+
> confirming that exact version in the official registry. From a local checkout, stay in the
|
|
323
|
+
> directory that owns `public-release.json` and call
|
|
324
|
+
> `node /absolute/path/to/skill-family-doc-render/bin/skill-family-doc-render.mjs --check-project --repo <name>`,
|
|
325
|
+
> replacing the placeholder with the real source path. The check is read-only:
|
|
289
326
|
> it renders in memory, compares against the on-disk `site-baseline.json`, writes
|
|
290
327
|
> nothing and touches no network or credentials. The CLI performs no Git writes; only an
|
|
291
328
|
> explicit `--assert-git` performs a read-only query of the Git index.
|
|
@@ -294,7 +331,9 @@ Apache-2.0
|
|
|
294
331
|
> **External write boundary:** rendering writes the declared target and a controlled sibling
|
|
295
332
|
> staging directory, both within the target's canonical parent, with contained path checks. An
|
|
296
333
|
> explicit `--refresh-coverage` writes only the declared coverage lock through an atomic file
|
|
297
|
-
> replacement. `--status`, `--check`, and `--check-project` write nothing
|
|
334
|
+
> replacement. `--status`, `--check`, and `--check-project` write nothing unless
|
|
335
|
+
> `--check-project` is given an explicit `--conclusion-output`, which exclusive-creates that
|
|
336
|
+
> proof file and never overwrites it. `--read-proof` writes nothing.
|
|
298
337
|
> Render, scan, baseline and `--assert-git` preflight failures happen before target changes.
|
|
299
338
|
> Staging is removed after success or a clearly pre-commit failure; post-commit or
|
|
300
339
|
> indeterminate failures may retain it to preserve diagnosable state.
|
|
@@ -307,6 +346,6 @@ Apache-2.0
|
|
|
307
346
|
Display help and run a read-only project check:
|
|
308
347
|
|
|
309
348
|
```sh
|
|
310
|
-
npx skill-family-doc-render@0.
|
|
311
|
-
npx skill-family-doc-render@0.
|
|
349
|
+
npx skill-family-doc-render@0.5.0 --help
|
|
350
|
+
npx skill-family-doc-render@0.5.0 --check-project --repo <name>
|
|
312
351
|
```
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
-
// CLI 入口:全部 argv 原样委托给 main()
|
|
3
|
+
// CLI 入口:全部 argv 原样委托给 main()。闭集解析、--help/-h、
|
|
4
|
+
// --check-project / --read-proof 互斥路由均由 main() 裁决。
|
|
4
5
|
import { main, RenderError } from '../scripts/render-public-site.mjs';
|
|
5
6
|
|
|
6
7
|
main(process.argv.slice(2), { cwd: process.cwd() })
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skill-family-doc-render",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Config-driven GitHub Pages knowledge-site renderer for skill-family projects (multi-repo capable).",
|
|
5
5
|
"author": "广州市风荷科技有限公司",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"marked": "18.0.11",
|
|
30
|
-
"skill-family-contracts": "0.
|
|
31
|
-
"skill-family-harness-node": "0.
|
|
30
|
+
"skill-family-contracts": "0.22.0",
|
|
31
|
+
"skill-family-harness-node": "0.22.0"
|
|
32
32
|
}
|
|
33
33
|
}
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// 文档族共同证明:把机械 --check-project 结果投影为 professional-conclusion,
|
|
3
|
+
// 并读取本族证明。结构校验与文件读写使用 Foundation 公共入口;本模块只保留
|
|
4
|
+
// DOCS 领域码、实际检查范围和原因转换。
|
|
5
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
6
|
+
import { basename, dirname, isAbsolute, join, resolve } from 'node:path';
|
|
7
|
+
import { findSchemaByObject, validateDocument } from 'skill-family-contracts';
|
|
8
|
+
import {
|
|
9
|
+
HARNESS_ERROR_KINDS,
|
|
10
|
+
publishFileExclusive,
|
|
11
|
+
readFileStrict,
|
|
12
|
+
resolveContained,
|
|
13
|
+
} from 'skill-family-harness-node';
|
|
14
|
+
import { RenderedLinksError } from './lib/rendered-links.mjs';
|
|
15
|
+
|
|
16
|
+
export const PROVIDER_ID = 'skill-family-doc-render';
|
|
17
|
+
|
|
18
|
+
export const DOCS_CODES = Object.freeze({
|
|
19
|
+
CLEAR: 'DOCS_CLEAR',
|
|
20
|
+
FINDINGS: 'DOCS_FINDINGS',
|
|
21
|
+
INCOMPLETE: 'DOCS_CHECK_INCOMPLETE',
|
|
22
|
+
UNAVAILABLE: 'DOCS_CHECK_UNAVAILABLE',
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export const CHECK_STAGE_IDS = Object.freeze([
|
|
26
|
+
'input-and-public-safety',
|
|
27
|
+
'coverage-freshness',
|
|
28
|
+
'in-memory-render-and-baseline',
|
|
29
|
+
'internal-links-and-resources',
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
const MECHANICAL_LIMITATION = '本证明只覆盖渲染器机械检查,不含中文语义审阅。';
|
|
33
|
+
|
|
34
|
+
const PROVIDER_VERSION = JSON.parse(
|
|
35
|
+
readFileSync(new URL('../package.json', import.meta.url), 'utf8'),
|
|
36
|
+
).version;
|
|
37
|
+
|
|
38
|
+
export class ProofError extends Error {
|
|
39
|
+
constructor(message, exitCode = 2, cause) {
|
|
40
|
+
super(message, cause === undefined ? undefined : { cause });
|
|
41
|
+
this.exitCode = exitCode;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function providerInfo(entry) {
|
|
46
|
+
return { id: PROVIDER_ID, version: PROVIDER_VERSION, entry };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function professionalSchemaRegistration() {
|
|
50
|
+
return findSchemaByObject('professional-conclusion');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function requireProfessionalSchemaId() {
|
|
54
|
+
const registration = professionalSchemaRegistration();
|
|
55
|
+
if (!registration?.$id || !registration.dialect) {
|
|
56
|
+
throw new ProofError(
|
|
57
|
+
'[skill-family-doc-render] 缺少 Foundation professional-conclusion Schema,需要精确安装 skill-family-contracts@0.22.0',
|
|
58
|
+
2,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
return registration;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function validateProfessionalConclusion(document) {
|
|
65
|
+
const registration = requireProfessionalSchemaId();
|
|
66
|
+
return validateDocument(document, {
|
|
67
|
+
schemaId: registration.$id,
|
|
68
|
+
dialect: registration.dialect,
|
|
69
|
+
policy: 'strict',
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function uniqueLimitations(items) {
|
|
74
|
+
const seen = new Set();
|
|
75
|
+
const result = [];
|
|
76
|
+
for (const item of items) {
|
|
77
|
+
if (typeof item !== 'string' || !item || seen.has(item)) continue;
|
|
78
|
+
seen.add(item);
|
|
79
|
+
result.push(item);
|
|
80
|
+
}
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function buildDocsConclusion({
|
|
85
|
+
entry,
|
|
86
|
+
subjectRef,
|
|
87
|
+
subjectRevision,
|
|
88
|
+
checked,
|
|
89
|
+
limitations,
|
|
90
|
+
completion,
|
|
91
|
+
code,
|
|
92
|
+
summary,
|
|
93
|
+
details,
|
|
94
|
+
}) {
|
|
95
|
+
const conclusion = {
|
|
96
|
+
schemaVersion: 1,
|
|
97
|
+
kind: 'skill-family.professional-conclusion',
|
|
98
|
+
provider: providerInfo(entry),
|
|
99
|
+
subject: subjectRevision
|
|
100
|
+
? { ref: subjectRef, revision: subjectRevision }
|
|
101
|
+
: { ref: subjectRef },
|
|
102
|
+
scope: {
|
|
103
|
+
checked: [...checked],
|
|
104
|
+
limitations: uniqueLimitations([MECHANICAL_LIMITATION, ...limitations]),
|
|
105
|
+
},
|
|
106
|
+
outcome: { completion, code, summary },
|
|
107
|
+
};
|
|
108
|
+
if (details !== undefined) conclusion.details = details;
|
|
109
|
+
const validation = validateProfessionalConclusion(conclusion);
|
|
110
|
+
if (!validation.valid) {
|
|
111
|
+
const detail = (validation.errors || []).map((error) => error.message).join('; ');
|
|
112
|
+
throw new ProofError(
|
|
113
|
+
`[skill-family-doc-render] 共同证明结构校验失败: ${validation.errorCode || 'invalid'}${detail ? `: ${detail}` : ''}`,
|
|
114
|
+
2,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
return validation.data;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function harnessKind(error) {
|
|
121
|
+
return error?.details?.kind;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function errorCauseChain(error) {
|
|
125
|
+
const seen = new Set();
|
|
126
|
+
const chain = [];
|
|
127
|
+
let current = error;
|
|
128
|
+
while (current && typeof current === 'object' && !seen.has(current)) {
|
|
129
|
+
seen.add(current);
|
|
130
|
+
chain.push(current);
|
|
131
|
+
current = current.cause;
|
|
132
|
+
}
|
|
133
|
+
return chain;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function hasRenderedLinkFailures(error) {
|
|
137
|
+
return errorCauseChain(error).some(
|
|
138
|
+
(item) => item instanceof RenderedLinksError && Array.isArray(item.failures) && item.failures.length > 0,
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function hasMissingResourceKind(error) {
|
|
143
|
+
return errorCauseChain(error).some(
|
|
144
|
+
(item) => harnessKind(item) === HARNESS_ERROR_KINDS.MISSING_RESOURCE,
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function isDomainFinding(exitCode, cause) {
|
|
149
|
+
if (exitCode !== 1) return false;
|
|
150
|
+
if (hasRenderedLinkFailures(cause)) return true;
|
|
151
|
+
return !hasMissingResourceKind(cause);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function mapScanToOutcome({
|
|
155
|
+
ok,
|
|
156
|
+
exitCode,
|
|
157
|
+
finished = [],
|
|
158
|
+
attempted = null,
|
|
159
|
+
notReached = [],
|
|
160
|
+
repoSelected = true,
|
|
161
|
+
cause = null,
|
|
162
|
+
}) {
|
|
163
|
+
if (ok) {
|
|
164
|
+
return {
|
|
165
|
+
completion: 'complete',
|
|
166
|
+
code: DOCS_CODES.CLEAR,
|
|
167
|
+
summary: '机械项目检查完成,未发现覆盖、渲染、链接或公开安全问题。',
|
|
168
|
+
checked: [...CHECK_STAGE_IDS],
|
|
169
|
+
limitations: [],
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
const started = Boolean(repoSelected && (finished.length || attempted));
|
|
173
|
+
if (!started) {
|
|
174
|
+
const remaining = attempted ? [attempted, ...notReached] : [...notReached];
|
|
175
|
+
return {
|
|
176
|
+
completion: 'not-performed',
|
|
177
|
+
code: DOCS_CODES.UNAVAILABLE,
|
|
178
|
+
summary: '未能开展机械项目检查。',
|
|
179
|
+
checked: [],
|
|
180
|
+
limitations: remaining.length ? remaining.map((id) => `未执行检查: ${id}`) : ['机械项目检查未开始。'],
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
const findings = isDomainFinding(exitCode, cause);
|
|
184
|
+
const checked = findings && attempted ? [...finished, attempted] : [...finished];
|
|
185
|
+
const limitations = [];
|
|
186
|
+
if (!findings && attempted) limitations.push(`未完成检查: ${attempted}`);
|
|
187
|
+
for (const id of notReached) limitations.push(`未执行检查: ${id}`);
|
|
188
|
+
if (findings) {
|
|
189
|
+
return {
|
|
190
|
+
completion: limitations.length ? 'partial' : 'complete',
|
|
191
|
+
code: DOCS_CODES.FINDINGS,
|
|
192
|
+
summary: '机械项目检查发现覆盖、渲染、链接或公开安全问题。',
|
|
193
|
+
checked,
|
|
194
|
+
limitations,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
return {
|
|
198
|
+
completion: checked.length ? 'partial' : 'not-performed',
|
|
199
|
+
code: DOCS_CODES.INCOMPLETE,
|
|
200
|
+
summary: '机械项目检查未完成所声明范围。',
|
|
201
|
+
checked,
|
|
202
|
+
limitations: limitations.length ? limitations : ['所需项目输入不足。'],
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function identifiableProvider(value) {
|
|
207
|
+
if (!value || typeof value !== 'object') return null;
|
|
208
|
+
const id = typeof value.id === 'string' && value.id ? value.id : null;
|
|
209
|
+
const version = typeof value.version === 'string' && value.version ? value.version : null;
|
|
210
|
+
const entry = typeof value.entry === 'string' && value.entry ? value.entry : null;
|
|
211
|
+
if (!id && !version && !entry) return null;
|
|
212
|
+
return {
|
|
213
|
+
...(id ? { id } : {}),
|
|
214
|
+
...(version ? { version } : {}),
|
|
215
|
+
...(entry ? { entry } : {}),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function readResult(status, reason, provider, conclusion) {
|
|
220
|
+
return {
|
|
221
|
+
status,
|
|
222
|
+
reason,
|
|
223
|
+
provider: provider || providerInfo('skill-family-doc-render --read-proof'),
|
|
224
|
+
conclusion,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function interpretDocsProof(document) {
|
|
229
|
+
const provider = identifiableProvider(document?.provider);
|
|
230
|
+
if (!document || typeof document !== 'object' || Array.isArray(document)) {
|
|
231
|
+
return readResult('unavailable', '证明不是 JSON 对象。', provider, null);
|
|
232
|
+
}
|
|
233
|
+
let validation;
|
|
234
|
+
try {
|
|
235
|
+
validation = validateProfessionalConclusion(document);
|
|
236
|
+
} catch (cause) {
|
|
237
|
+
if (cause instanceof ProofError) {
|
|
238
|
+
return readResult('unavailable', cause.message, provider, null);
|
|
239
|
+
}
|
|
240
|
+
throw cause;
|
|
241
|
+
}
|
|
242
|
+
if (!validation.valid) {
|
|
243
|
+
return readResult(
|
|
244
|
+
'unavailable',
|
|
245
|
+
`证明不符合 professional-conclusion 结构: ${validation.errorCode || 'invalid'}`,
|
|
246
|
+
provider,
|
|
247
|
+
null,
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
const conclusion = validation.data;
|
|
251
|
+
const recorded = conclusion.provider;
|
|
252
|
+
if (recorded.id !== PROVIDER_ID) {
|
|
253
|
+
return readResult(
|
|
254
|
+
'unavailable',
|
|
255
|
+
`证明提供方不是 ${PROVIDER_ID}`,
|
|
256
|
+
recorded,
|
|
257
|
+
null,
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
const code = conclusion.outcome.code;
|
|
261
|
+
const known = new Set(Object.values(DOCS_CODES));
|
|
262
|
+
if (!known.has(code)) {
|
|
263
|
+
return readResult(
|
|
264
|
+
'unavailable',
|
|
265
|
+
`未知同版本领域码: ${code}`,
|
|
266
|
+
recorded,
|
|
267
|
+
conclusion,
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
if (code === DOCS_CODES.CLEAR && conclusion.outcome.completion === 'complete') {
|
|
271
|
+
return readResult('pass', conclusion.outcome.summary, recorded, conclusion);
|
|
272
|
+
}
|
|
273
|
+
return readResult('not_pass', conclusion.outcome.summary, recorded, conclusion);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export function readProofExitCode(status) {
|
|
277
|
+
if (status === 'pass') return 0;
|
|
278
|
+
if (status === 'not_pass') return 1;
|
|
279
|
+
return 2;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export async function readProfessionalProof({ proofRoot, proofPath }) {
|
|
283
|
+
const root = resolve(proofRoot);
|
|
284
|
+
const entry = 'skill-family-doc-render --read-proof';
|
|
285
|
+
let relative;
|
|
286
|
+
try {
|
|
287
|
+
await resolveContained(root, proofPath);
|
|
288
|
+
relative = proofPath;
|
|
289
|
+
} catch (cause) {
|
|
290
|
+
return readResult(
|
|
291
|
+
'unavailable',
|
|
292
|
+
`证明路径未收容于 proof-root: ${cause.message}`,
|
|
293
|
+
providerInfo(entry),
|
|
294
|
+
null,
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
let receipt;
|
|
298
|
+
try {
|
|
299
|
+
receipt = await readFileStrict(root, relative, { encoding: 'utf8' });
|
|
300
|
+
} catch (cause) {
|
|
301
|
+
const kind = harnessKind(cause);
|
|
302
|
+
let reason = `无法读取证明: ${cause.message}`;
|
|
303
|
+
if (kind === HARNESS_ERROR_KINDS.MISSING_RESOURCE) reason = `证明文件不存在: ${relative}`;
|
|
304
|
+
if (kind === HARNESS_ERROR_KINDS.PATH_TRAVERSAL || kind === HARNESS_ERROR_KINDS.ABSOLUTE_PATH) {
|
|
305
|
+
reason = `证明路径未收容于 proof-root: ${cause.message}`;
|
|
306
|
+
}
|
|
307
|
+
return readResult('unavailable', reason, providerInfo(entry), null);
|
|
308
|
+
}
|
|
309
|
+
let parsed;
|
|
310
|
+
try {
|
|
311
|
+
parsed = JSON.parse(receipt.content);
|
|
312
|
+
} catch (cause) {
|
|
313
|
+
return readResult(
|
|
314
|
+
'unavailable',
|
|
315
|
+
`证明 JSON 损坏: ${cause.message}`,
|
|
316
|
+
providerInfo(entry),
|
|
317
|
+
null,
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
return interpretDocsProof(parsed);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function splitAbsoluteOutput(absolutePath) {
|
|
324
|
+
const target = resolve(absolutePath);
|
|
325
|
+
if (!isAbsolute(target)) {
|
|
326
|
+
throw new ProofError('[skill-family-doc-render] --conclusion-output 必须是绝对路径', 2);
|
|
327
|
+
}
|
|
328
|
+
let directory = dirname(target);
|
|
329
|
+
const segments = [basename(target)];
|
|
330
|
+
while (!existsSync(directory)) {
|
|
331
|
+
const parent = dirname(directory);
|
|
332
|
+
if (parent === directory) {
|
|
333
|
+
throw new ProofError(`[skill-family-doc-render] 无法定位证明输出根目录: ${target}`, 2);
|
|
334
|
+
}
|
|
335
|
+
segments.unshift(basename(directory));
|
|
336
|
+
directory = parent;
|
|
337
|
+
}
|
|
338
|
+
return { root: directory, relPath: join(...segments), target };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export async function writeProfessionalConclusion(absolutePath, conclusion) {
|
|
342
|
+
const { root, relPath, target } = splitAbsoluteOutput(absolutePath);
|
|
343
|
+
const body = `${JSON.stringify(conclusion, null, 2)}\n`;
|
|
344
|
+
try {
|
|
345
|
+
const receipt = await publishFileExclusive(root, relPath, body, { createParents: true });
|
|
346
|
+
return { path: receipt.path || target, sha256: receipt.sha256 };
|
|
347
|
+
} catch (cause) {
|
|
348
|
+
if (harnessKind(cause) === HARNESS_ERROR_KINDS.EXCLUSIVE_PUBLISH_CONFLICT) {
|
|
349
|
+
throw new ProofError(`[skill-family-doc-render] 证明输出已存在,拒绝覆盖: ${target}`, 2, cause);
|
|
350
|
+
}
|
|
351
|
+
if (cause instanceof ProofError) throw cause;
|
|
352
|
+
throw new ProofError(`[skill-family-doc-render] 证明输出失败: ${cause.message}`, 2, cause);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export function conclusionEntryForRepo(repoName) {
|
|
357
|
+
return `skill-family-doc-render --check-project --repo ${repoName}`;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export { MECHANICAL_LIMITATION };
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
// 可预期失败统一封装为 RenderError 携带 exitCode。
|
|
28
28
|
import { existsSync, readFileSync } from 'node:fs';
|
|
29
29
|
import { readFile, readdir, rm, mkdtemp, realpath, stat, glob } from 'node:fs/promises';
|
|
30
|
-
import { resolve, relative, extname, join, dirname, basename, posix } from 'node:path';
|
|
30
|
+
import { resolve, relative, extname, join, dirname, basename, posix, isAbsolute } from 'node:path';
|
|
31
31
|
import { fileURLToPath } from 'node:url';
|
|
32
32
|
import { execFileSync } from 'node:child_process';
|
|
33
33
|
import {
|
|
@@ -47,6 +47,16 @@ import { compileMarkdownV1, replaceMarkdownTokens } from './lib/markdown-v1.mjs'
|
|
|
47
47
|
import { assertSafeSvg } from './lib/svg-safety.mjs';
|
|
48
48
|
import { checkRenderedLinks } from './lib/rendered-links.mjs';
|
|
49
49
|
import { renderEditorialSite } from '../templates/editorial/index.mjs';
|
|
50
|
+
import {
|
|
51
|
+
CHECK_STAGE_IDS,
|
|
52
|
+
ProofError,
|
|
53
|
+
buildDocsConclusion,
|
|
54
|
+
conclusionEntryForRepo,
|
|
55
|
+
mapScanToOutcome,
|
|
56
|
+
readProfessionalProof,
|
|
57
|
+
readProofExitCode,
|
|
58
|
+
writeProfessionalConclusion,
|
|
59
|
+
} from './professional-conclusion.mjs';
|
|
50
60
|
|
|
51
61
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
52
62
|
|
|
@@ -284,9 +294,22 @@ async function computeCoverageSnapshot(sourceRoot, repo) {
|
|
|
284
294
|
return { snapshot, versionSources };
|
|
285
295
|
}
|
|
286
296
|
|
|
297
|
+
function gitReadEnv(extra = {}) {
|
|
298
|
+
return { ...process.env, GIT_OPTIONAL_LOCKS: '0', ...extra };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function gitReadArgv(args) {
|
|
302
|
+
return ['--no-optional-locks', '-c', 'core.fsmonitor=false', ...args];
|
|
303
|
+
}
|
|
304
|
+
|
|
287
305
|
function git(root, args, repoName) {
|
|
288
306
|
try {
|
|
289
|
-
return execFileSync('git', args, {
|
|
307
|
+
return execFileSync('git', gitReadArgv(args), {
|
|
308
|
+
cwd: root,
|
|
309
|
+
encoding: 'utf8',
|
|
310
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
311
|
+
env: gitReadEnv(),
|
|
312
|
+
}).trim();
|
|
290
313
|
} catch (cause) {
|
|
291
314
|
const detail = String(cause.stderr || cause.message).trim();
|
|
292
315
|
throw configError(repoName, `Git 命令失败: git ${args.join(' ')}${detail ? `: ${detail}` : ''}`, cause);
|
|
@@ -519,7 +542,11 @@ function assertGitTracked(root, repo, targetRel, files) {
|
|
|
519
542
|
let missing = 0;
|
|
520
543
|
for (const f of expected) {
|
|
521
544
|
try {
|
|
522
|
-
execFileSync('git', ['ls-files', '--error-unmatch', f], {
|
|
545
|
+
execFileSync('git', gitReadArgv(['ls-files', '--error-unmatch', f]), {
|
|
546
|
+
cwd: root,
|
|
547
|
+
stdio: 'pipe',
|
|
548
|
+
env: gitReadEnv(),
|
|
549
|
+
});
|
|
523
550
|
} catch (cause) {
|
|
524
551
|
if (cause?.status === 1) {
|
|
525
552
|
console.error(`[assert-git] ${repo.name}: 未被 Git 索引跟踪(请先 git add): ${f}`);
|
|
@@ -771,26 +798,164 @@ function projectCheckError(repoName, cause, fallbackExitCode = 1) {
|
|
|
771
798
|
);
|
|
772
799
|
}
|
|
773
800
|
|
|
801
|
+
function attachDocsCheck(error, finished, attempted, notReached) {
|
|
802
|
+
error.docsCheck = { finished, attempted, notReached };
|
|
803
|
+
return error;
|
|
804
|
+
}
|
|
805
|
+
|
|
774
806
|
async function checkProject(root, repo, release, leakLiterals) {
|
|
807
|
+
const finished = [];
|
|
808
|
+
const stages = [
|
|
809
|
+
{
|
|
810
|
+
id: CHECK_STAGE_IDS[0],
|
|
811
|
+
run: async () => {
|
|
812
|
+
await renderRepo(root, repo, release, leakLiterals, { check: false, assertGit: false, inputOnly: true });
|
|
813
|
+
console.log(`[render-public-site --check-project] ${repo.name}: 输入、公开安全与正文机械合同通过`);
|
|
814
|
+
},
|
|
815
|
+
},
|
|
816
|
+
{
|
|
817
|
+
id: CHECK_STAGE_IDS[1],
|
|
818
|
+
run: async () => {
|
|
819
|
+
await reportCoverageStatus(root, repo);
|
|
820
|
+
},
|
|
821
|
+
},
|
|
822
|
+
{
|
|
823
|
+
id: CHECK_STAGE_IDS[2],
|
|
824
|
+
run: async () => {
|
|
825
|
+
await renderRepo(root, repo, release, leakLiterals, { check: true, assertGit: false });
|
|
826
|
+
},
|
|
827
|
+
},
|
|
828
|
+
{
|
|
829
|
+
id: CHECK_STAGE_IDS[3],
|
|
830
|
+
run: async () => {
|
|
831
|
+
const sourceRoot = await repoSourceRoot(root, repo);
|
|
832
|
+
const targetDir = await containedOrConfig(sourceRoot, repo.site.target, repo.name, 'site.target');
|
|
833
|
+
const pagesDoc = await readJsonContained(sourceRoot, join(repo.site.dir, repo.site.pages), 'pages.json', repo.name);
|
|
834
|
+
await checkRenderedLinks({ targetRoot: targetDir, pageIds: pagesDoc.pages.map((page) => page.id) });
|
|
835
|
+
console.log(`[render-public-site --check-project] ${repo.name}: 内部链接与资源通过`);
|
|
836
|
+
console.log(`[render-public-site --check-project] ${repo.name}: public-release.json 只读项目检查通过`);
|
|
837
|
+
},
|
|
838
|
+
},
|
|
839
|
+
];
|
|
775
840
|
try {
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
const sourceRoot = await repoSourceRoot(root, repo);
|
|
781
|
-
const targetDir = await containedOrConfig(sourceRoot, repo.site.target, repo.name, 'site.target');
|
|
782
|
-
const pagesDoc = await readJsonContained(sourceRoot, join(repo.site.dir, repo.site.pages), 'pages.json', repo.name);
|
|
783
|
-
await checkRenderedLinks({ targetRoot: targetDir, pageIds: pagesDoc.pages.map((page) => page.id) });
|
|
784
|
-
console.log(`[render-public-site --check-project] ${repo.name}: 内部链接与资源通过`);
|
|
785
|
-
console.log(`[render-public-site --check-project] ${repo.name}: public-release.json 只读项目检查通过`);
|
|
841
|
+
for (const stage of stages) {
|
|
842
|
+
await stage.run();
|
|
843
|
+
finished.push(stage.id);
|
|
844
|
+
}
|
|
786
845
|
} catch (cause) {
|
|
787
|
-
|
|
846
|
+
const remaining = stages.map((stage) => stage.id).filter((id) => !finished.includes(id));
|
|
847
|
+
throw attachDocsCheck(
|
|
848
|
+
projectCheckError(repo.name, cause),
|
|
849
|
+
finished,
|
|
850
|
+
remaining[0] ?? null,
|
|
851
|
+
remaining.slice(1),
|
|
852
|
+
);
|
|
788
853
|
}
|
|
789
854
|
}
|
|
790
855
|
|
|
856
|
+
function subjectRevision(root) {
|
|
857
|
+
try {
|
|
858
|
+
return execFileSync('git', gitReadArgv(['rev-parse', 'HEAD']), {
|
|
859
|
+
cwd: root,
|
|
860
|
+
encoding: 'utf8',
|
|
861
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
862
|
+
env: gitReadEnv(),
|
|
863
|
+
}).trim();
|
|
864
|
+
} catch {
|
|
865
|
+
return undefined;
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
function asRenderError(error) {
|
|
870
|
+
if (error instanceof RenderError) return error;
|
|
871
|
+
if (error instanceof ProofError) return new RenderError(error.message, error.exitCode, error);
|
|
872
|
+
return error;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
async function emitProjectConclusion({
|
|
876
|
+
outputPath,
|
|
877
|
+
root,
|
|
878
|
+
repoName,
|
|
879
|
+
ok,
|
|
880
|
+
exitCode,
|
|
881
|
+
message,
|
|
882
|
+
cause = null,
|
|
883
|
+
docsCheck,
|
|
884
|
+
repoSelected = true,
|
|
885
|
+
}) {
|
|
886
|
+
const outcome = mapScanToOutcome({
|
|
887
|
+
ok,
|
|
888
|
+
exitCode,
|
|
889
|
+
finished: docsCheck?.finished || [],
|
|
890
|
+
attempted: docsCheck?.attempted || null,
|
|
891
|
+
notReached: docsCheck?.notReached || (ok ? [] : [...CHECK_STAGE_IDS]),
|
|
892
|
+
repoSelected,
|
|
893
|
+
cause,
|
|
894
|
+
});
|
|
895
|
+
const conclusion = buildDocsConclusion({
|
|
896
|
+
entry: conclusionEntryForRepo(repoName),
|
|
897
|
+
subjectRef: `${root}::${repoName}`,
|
|
898
|
+
subjectRevision: subjectRevision(root),
|
|
899
|
+
checked: outcome.checked,
|
|
900
|
+
limitations: outcome.limitations,
|
|
901
|
+
completion: outcome.completion,
|
|
902
|
+
code: outcome.code,
|
|
903
|
+
summary: outcome.summary,
|
|
904
|
+
details: {
|
|
905
|
+
mechanical: true,
|
|
906
|
+
repo: repoName,
|
|
907
|
+
exitCode,
|
|
908
|
+
findings: ok ? [] : [message],
|
|
909
|
+
},
|
|
910
|
+
});
|
|
911
|
+
const receipt = await writeProfessionalConclusion(outputPath, conclusion);
|
|
912
|
+
console.log(`[render-public-site --check-project] ${repoName}: professional conclusion written to ${receipt.path}`);
|
|
913
|
+
return receipt;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
async function emitProjectConclusionOrThrow(args, primaryError) {
|
|
917
|
+
try {
|
|
918
|
+
await emitProjectConclusion(args);
|
|
919
|
+
} catch (cause) {
|
|
920
|
+
const writeError = asRenderError(cause);
|
|
921
|
+
if (primaryError) {
|
|
922
|
+
throw new RenderError(`${primaryError.message}\n${writeError.message}`, 2, writeError);
|
|
923
|
+
}
|
|
924
|
+
throw writeError;
|
|
925
|
+
}
|
|
926
|
+
if (primaryError) throw primaryError;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
function takeValue(argv, i, flag, flags, key) {
|
|
930
|
+
if (flags[key] !== null) {
|
|
931
|
+
throw new RenderError(`[render-public-site] 参数错误: ${flag} 重复出现,取值语义不明确`, 2);
|
|
932
|
+
}
|
|
933
|
+
const value = argv[i + 1];
|
|
934
|
+
if (value === undefined || value === '' || value.startsWith('-')) {
|
|
935
|
+
throw new RenderError(`[render-public-site] ${flag} 后必须跟一个有效取值(不能缺省,且值不能是另一个选项)`, 2);
|
|
936
|
+
}
|
|
937
|
+
flags[key] = value;
|
|
938
|
+
return i + 1;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
function projectModeError(flags, message) {
|
|
942
|
+
const error = projectCheckError(
|
|
943
|
+
flags.repoArg ?? '<missing>',
|
|
944
|
+
new RenderError(message, 2),
|
|
945
|
+
2,
|
|
946
|
+
);
|
|
947
|
+
error.repoArg = flags.repoArg;
|
|
948
|
+
if (flags.conclusionOutput && isAbsolute(flags.conclusionOutput)) {
|
|
949
|
+
error.conclusionOutput = flags.conclusionOutput;
|
|
950
|
+
}
|
|
951
|
+
return error;
|
|
952
|
+
}
|
|
953
|
+
|
|
791
954
|
// === 主流程 ===
|
|
792
955
|
// 闭集参数解析:允许参数只有 --check / --assert-git / --status /
|
|
793
|
-
// --refresh-coverage / --check-project / --
|
|
956
|
+
// --refresh-coverage / --check-project / --conclusion-output <absolute-path> /
|
|
957
|
+
// --read-proof / --proof-root <root> / --proof <relative> / --repo <name> /
|
|
958
|
+
// --help / -h。
|
|
794
959
|
// 未知选项、游离位置参数、重复取值参数、--repo 缺值或值本身是另一个选项,
|
|
795
960
|
// 均以 RenderError(exit 2)失败,错误信息指出具体参数。不引入 CLI 框架。
|
|
796
961
|
// --help / -h 由 main() 统一呈现:闭集解析先于配置读取与写盘,打印 USAGE_TEXT 后退出 0。
|
|
@@ -809,8 +974,12 @@ Usage:
|
|
|
809
974
|
refresh only site-coverage-lock.json
|
|
810
975
|
skill-family-doc-render --check-project --repo <name>
|
|
811
976
|
read-only input, coverage, render and link checks
|
|
977
|
+
skill-family-doc-render --check-project --repo <name> --conclusion-output <absolute-path>
|
|
978
|
+
same read-only checks, then exclusive-write a professional conclusion
|
|
979
|
+
skill-family-doc-render --read-proof --proof-root <root> --proof <relative>
|
|
980
|
+
read one docs-family professional conclusion; do not rescan
|
|
812
981
|
|
|
813
|
-
Reads public-release.json from the current working directory.`;
|
|
982
|
+
Reads public-release.json from the current working directory except --read-proof.`;
|
|
814
983
|
|
|
815
984
|
function parseArgs(argv) {
|
|
816
985
|
const flags = {
|
|
@@ -819,7 +988,11 @@ function parseArgs(argv) {
|
|
|
819
988
|
status: false,
|
|
820
989
|
refreshCoverage: false,
|
|
821
990
|
checkProject: false,
|
|
991
|
+
readProof: false,
|
|
822
992
|
repoArg: null,
|
|
993
|
+
conclusionOutput: null,
|
|
994
|
+
proofRoot: null,
|
|
995
|
+
proof: null,
|
|
823
996
|
help: false,
|
|
824
997
|
};
|
|
825
998
|
let i = 0;
|
|
@@ -835,6 +1008,8 @@ function parseArgs(argv) {
|
|
|
835
1008
|
flags.refreshCoverage = true;
|
|
836
1009
|
} else if (arg === '--check-project') {
|
|
837
1010
|
flags.checkProject = true;
|
|
1011
|
+
} else if (arg === '--read-proof') {
|
|
1012
|
+
flags.readProof = true;
|
|
838
1013
|
} else if (arg === '--help' || arg === '-h') {
|
|
839
1014
|
flags.help = true;
|
|
840
1015
|
} else if (arg === '--repo') {
|
|
@@ -846,7 +1021,13 @@ function parseArgs(argv) {
|
|
|
846
1021
|
throw new RenderError('[render-public-site] --repo 后必须跟一个有效的 repo 名(不能缺省,且值不能是另一个选项)', 2);
|
|
847
1022
|
}
|
|
848
1023
|
flags.repoArg = value;
|
|
849
|
-
i += 1;
|
|
1024
|
+
i += 1;
|
|
1025
|
+
} else if (arg === '--conclusion-output') {
|
|
1026
|
+
i = takeValue(argv, i, '--conclusion-output', flags, 'conclusionOutput');
|
|
1027
|
+
} else if (arg === '--proof-root') {
|
|
1028
|
+
i = takeValue(argv, i, '--proof-root', flags, 'proofRoot');
|
|
1029
|
+
} else if (arg === '--proof') {
|
|
1030
|
+
i = takeValue(argv, i, '--proof', flags, 'proof');
|
|
850
1031
|
} else if (arg.startsWith('-')) {
|
|
851
1032
|
throw new RenderError(`[render-public-site] 参数错误: 未知选项 ${arg}`, 2);
|
|
852
1033
|
} else {
|
|
@@ -863,19 +1044,29 @@ function parseArgs(argv) {
|
|
|
863
1044
|
if ((flags.status || flags.refreshCoverage) && flags.repoArg === null) {
|
|
864
1045
|
throw new RenderError('[render-public-site] 参数错误: --status 与 --refresh-coverage 必须配合 --repo <name>', 2);
|
|
865
1046
|
}
|
|
1047
|
+
if (flags.readProof && flags.checkProject) {
|
|
1048
|
+
throw new RenderError('[render-public-site] 参数错误: --check-project 与 --read-proof 互斥', 2);
|
|
1049
|
+
}
|
|
1050
|
+
if (flags.readProof && (flags.check || flags.assertGit || flags.status || flags.refreshCoverage || flags.help || flags.conclusionOutput || flags.repoArg)) {
|
|
1051
|
+
throw new RenderError('[render-public-site] 参数错误: --read-proof 不能与检查、覆盖、Git、帮助或 --conclusion-output 组合', 2);
|
|
1052
|
+
}
|
|
1053
|
+
if (flags.readProof && (flags.proofRoot === null || flags.proof === null)) {
|
|
1054
|
+
throw new RenderError('[render-public-site] 参数错误: --read-proof 必须配合 --proof-root <root> 与 --proof <relative>', 2);
|
|
1055
|
+
}
|
|
1056
|
+
if (!flags.readProof && (flags.proofRoot !== null || flags.proof !== null)) {
|
|
1057
|
+
throw new RenderError('[render-public-site] 参数错误: --proof-root 与 --proof 只能与 --read-proof 一起使用', 2);
|
|
1058
|
+
}
|
|
1059
|
+
if (flags.conclusionOutput && !flags.checkProject) {
|
|
1060
|
+
throw new RenderError('[render-public-site] 参数错误: --conclusion-output 只能与 --check-project 一起使用', 2);
|
|
1061
|
+
}
|
|
1062
|
+
if (flags.conclusionOutput && !isAbsolute(flags.conclusionOutput)) {
|
|
1063
|
+
throw projectModeError(flags, '参数错误: --conclusion-output 必须是绝对路径');
|
|
1064
|
+
}
|
|
866
1065
|
if (flags.checkProject && (flags.check || flags.assertGit || flags.status || flags.refreshCoverage || flags.help)) {
|
|
867
|
-
throw
|
|
868
|
-
flags.repoArg ?? '<missing>',
|
|
869
|
-
new RenderError('参数错误: --check-project 不能与其他检查、覆盖、Git 或帮助模式组合', 2),
|
|
870
|
-
2,
|
|
871
|
-
);
|
|
1066
|
+
throw projectModeError(flags, '参数错误: --check-project 不能与其他检查、覆盖、Git 或帮助模式组合');
|
|
872
1067
|
}
|
|
873
1068
|
if (flags.checkProject && flags.repoArg === null) {
|
|
874
|
-
throw
|
|
875
|
-
'<missing>',
|
|
876
|
-
new RenderError('参数错误: --check-project 必须配合 --repo <name>', 2),
|
|
877
|
-
2,
|
|
878
|
-
);
|
|
1069
|
+
throw projectModeError(flags, '参数错误: --check-project 必须配合 --repo <name>');
|
|
879
1070
|
}
|
|
880
1071
|
return flags;
|
|
881
1072
|
}
|
|
@@ -886,7 +1077,20 @@ function parseArgs(argv) {
|
|
|
886
1077
|
// 单个 repo 内部(含泄漏扫描命中)保持 fail-fast。
|
|
887
1078
|
export async function main(argv = process.argv.slice(2), { cwd = process.cwd() } = {}) {
|
|
888
1079
|
// 闭集解析在读取任何配置或写盘之前完成;参数失败时目标树一个字节都不动。
|
|
889
|
-
const
|
|
1080
|
+
const parsed = parseArgs(argv);
|
|
1081
|
+
const {
|
|
1082
|
+
check,
|
|
1083
|
+
assertGit,
|
|
1084
|
+
status,
|
|
1085
|
+
refreshCoverage,
|
|
1086
|
+
checkProject: projectCheck,
|
|
1087
|
+
readProof,
|
|
1088
|
+
repoArg,
|
|
1089
|
+
conclusionOutput,
|
|
1090
|
+
proofRoot,
|
|
1091
|
+
proof,
|
|
1092
|
+
help,
|
|
1093
|
+
} = parsed;
|
|
890
1094
|
if (help) {
|
|
891
1095
|
console.log(USAGE_TEXT);
|
|
892
1096
|
return 0;
|
|
@@ -894,18 +1098,54 @@ export async function main(argv = process.argv.slice(2), { cwd = process.cwd() }
|
|
|
894
1098
|
|
|
895
1099
|
const root = resolve(cwd);
|
|
896
1100
|
|
|
1101
|
+
if (readProof) {
|
|
1102
|
+
const result = await readProfessionalProof({ proofRoot, proofPath: proof });
|
|
1103
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1104
|
+
const code = readProofExitCode(result.status);
|
|
1105
|
+
if (code === 0) return 0;
|
|
1106
|
+
throw new RenderError(result.reason, code);
|
|
1107
|
+
}
|
|
1108
|
+
|
|
897
1109
|
// --- 读取发布配置 ---
|
|
898
1110
|
const releasePath = resolve(root, 'public-release.json');
|
|
899
1111
|
if (!existsSync(releasePath)) {
|
|
900
1112
|
const cause = new RenderError('[render-public-site] 未找到 public-release.json', 2);
|
|
901
|
-
|
|
1113
|
+
const error = projectCheck ? projectCheckError(repoArg, cause, 2) : cause;
|
|
1114
|
+
if (projectCheck && conclusionOutput) {
|
|
1115
|
+
await emitProjectConclusionOrThrow({
|
|
1116
|
+
outputPath: conclusionOutput,
|
|
1117
|
+
root,
|
|
1118
|
+
repoName: repoArg,
|
|
1119
|
+
ok: false,
|
|
1120
|
+
exitCode: 2,
|
|
1121
|
+
message: error.message,
|
|
1122
|
+
cause: error,
|
|
1123
|
+
repoSelected: false,
|
|
1124
|
+
}, error);
|
|
1125
|
+
}
|
|
1126
|
+
throw error;
|
|
902
1127
|
}
|
|
903
1128
|
let release;
|
|
904
1129
|
try {
|
|
905
1130
|
release = await readJsonContained(root, 'public-release.json', 'public-release.json', '(workspace)');
|
|
906
1131
|
assertValid(release, publicReleaseSchema, 'public-release.json');
|
|
907
1132
|
} catch (cause) {
|
|
908
|
-
if (projectCheck)
|
|
1133
|
+
if (projectCheck) {
|
|
1134
|
+
const error = projectCheckError(repoArg, cause, 2);
|
|
1135
|
+
if (conclusionOutput) {
|
|
1136
|
+
await emitProjectConclusionOrThrow({
|
|
1137
|
+
outputPath: conclusionOutput,
|
|
1138
|
+
root,
|
|
1139
|
+
repoName: repoArg,
|
|
1140
|
+
ok: false,
|
|
1141
|
+
exitCode: 2,
|
|
1142
|
+
message: error.message,
|
|
1143
|
+
cause: error,
|
|
1144
|
+
repoSelected: Boolean(repoArg),
|
|
1145
|
+
}, error);
|
|
1146
|
+
}
|
|
1147
|
+
throw error;
|
|
1148
|
+
}
|
|
909
1149
|
throw cause;
|
|
910
1150
|
}
|
|
911
1151
|
|
|
@@ -921,12 +1161,38 @@ export async function main(argv = process.argv.slice(2), { cwd = process.cwd() }
|
|
|
921
1161
|
const hit = selected.find((r) => r.name === repoArg);
|
|
922
1162
|
if (!hit) {
|
|
923
1163
|
const cause = new RenderError(`[render-public-site] 未找到带 site 的 repo: ${repoArg}`, 2);
|
|
924
|
-
|
|
1164
|
+
const error = projectCheck ? projectCheckError(repoArg, cause, 2) : cause;
|
|
1165
|
+
if (projectCheck && conclusionOutput) {
|
|
1166
|
+
await emitProjectConclusionOrThrow({
|
|
1167
|
+
outputPath: conclusionOutput,
|
|
1168
|
+
root,
|
|
1169
|
+
repoName: repoArg,
|
|
1170
|
+
ok: false,
|
|
1171
|
+
exitCode: 2,
|
|
1172
|
+
message: error.message,
|
|
1173
|
+
cause: error,
|
|
1174
|
+
repoSelected: false,
|
|
1175
|
+
}, error);
|
|
1176
|
+
}
|
|
1177
|
+
throw error;
|
|
925
1178
|
}
|
|
926
1179
|
selected = [hit];
|
|
927
1180
|
}
|
|
928
1181
|
if (!selected.length) {
|
|
929
|
-
|
|
1182
|
+
const error = new RenderError('[render-public-site] public-release.json 中没有带 site 字段的 repo', 2);
|
|
1183
|
+
if (projectCheck && conclusionOutput) {
|
|
1184
|
+
await emitProjectConclusionOrThrow({
|
|
1185
|
+
outputPath: conclusionOutput,
|
|
1186
|
+
root,
|
|
1187
|
+
repoName: repoArg ?? '<missing>',
|
|
1188
|
+
ok: false,
|
|
1189
|
+
exitCode: 2,
|
|
1190
|
+
message: error.message,
|
|
1191
|
+
cause: error,
|
|
1192
|
+
repoSelected: false,
|
|
1193
|
+
}, error);
|
|
1194
|
+
}
|
|
1195
|
+
throw error;
|
|
930
1196
|
}
|
|
931
1197
|
|
|
932
1198
|
const results = [];
|
|
@@ -934,8 +1200,49 @@ export async function main(argv = process.argv.slice(2), { cwd = process.cwd() }
|
|
|
934
1200
|
try {
|
|
935
1201
|
if (status) await reportCoverageStatus(root, repo);
|
|
936
1202
|
else if (refreshCoverage) await refreshCoverageLock(root, repo);
|
|
937
|
-
else if (projectCheck)
|
|
938
|
-
|
|
1203
|
+
else if (projectCheck) {
|
|
1204
|
+
let scanError = null;
|
|
1205
|
+
try {
|
|
1206
|
+
await checkProject(root, repo, release, leakLiterals);
|
|
1207
|
+
} catch (cause) {
|
|
1208
|
+
scanError = cause;
|
|
1209
|
+
}
|
|
1210
|
+
if (conclusionOutput) {
|
|
1211
|
+
try {
|
|
1212
|
+
await emitProjectConclusion({
|
|
1213
|
+
outputPath: conclusionOutput,
|
|
1214
|
+
root,
|
|
1215
|
+
repoName: repo.name,
|
|
1216
|
+
ok: !scanError,
|
|
1217
|
+
exitCode: scanError ? (Number.isInteger(scanError.exitCode) ? scanError.exitCode : 1) : 0,
|
|
1218
|
+
message: scanError ? scanError.message : 'ok',
|
|
1219
|
+
cause: scanError,
|
|
1220
|
+
docsCheck: scanError?.docsCheck || { finished: [...CHECK_STAGE_IDS], attempted: null, notReached: [] },
|
|
1221
|
+
repoSelected: true,
|
|
1222
|
+
});
|
|
1223
|
+
} catch (cause) {
|
|
1224
|
+
const writeError = asRenderError(cause);
|
|
1225
|
+
results.push({
|
|
1226
|
+
name: repo.name,
|
|
1227
|
+
ok: false,
|
|
1228
|
+
exitCode: 2,
|
|
1229
|
+
message: scanError ? `${scanError.message}\n${writeError.message}` : writeError.message,
|
|
1230
|
+
cause: writeError,
|
|
1231
|
+
});
|
|
1232
|
+
continue;
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
if (scanError) {
|
|
1236
|
+
results.push({
|
|
1237
|
+
name: repo.name,
|
|
1238
|
+
ok: false,
|
|
1239
|
+
exitCode: Number.isInteger(scanError.exitCode) ? scanError.exitCode : 1,
|
|
1240
|
+
message: scanError.message,
|
|
1241
|
+
cause: scanError,
|
|
1242
|
+
});
|
|
1243
|
+
continue;
|
|
1244
|
+
}
|
|
1245
|
+
} else await renderRepo(root, repo, release, leakLiterals, { check, assertGit });
|
|
939
1246
|
results.push({ name: repo.name, ok: true });
|
|
940
1247
|
} catch (err) {
|
|
941
1248
|
results.push({
|