mirror of
https://github.com/titanwings/colleague-skill.git
synced 2026-10-17 05:21:02 +08:00
fix(contract): align CLI surface, docs, and executable release checks
Refuse PDF on parse-doc with office/unsupported-format. Replace text-grep release criteria with executed migrate, install, and run-gates checks. Delete ci.yml. Refresh STATUS to the atomic stack that stops at dot-skill-test.
This commit is contained in:
@@ -1,96 +0,0 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
# `ds/**` is not decoration: the 19 per-feature PRs are a **stack** — each targets
|
||||
# the branch before it (`ds/02` → `ds/01`, … , `ds/21` → `ds/20`), and `ds/01`
|
||||
# targets `pre-v2-baseline`. With this list limited to the three integration
|
||||
# branches, not one of them could ever run CI: every PR sat at "no checks
|
||||
# reported" while its body claimed tests. A base branch that no trigger matches is
|
||||
# a gate that does not exist.
|
||||
push:
|
||||
branches: [dot-skill-test, dot-skill, main, 'ds/**']
|
||||
pull_request:
|
||||
branches: [dot-skill-test, dot-skill, main, 'ds/**']
|
||||
# Manual re-runs without pushing anything.
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Node ${{ matrix.node-version }}
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
node-version: ["20", "22"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
|
||||
- name: Syntax-check every module
|
||||
run: |
|
||||
find bin src scripts tests -name '*.mjs' -print0 | xargs -0 -n1 node --check
|
||||
|
||||
- name: Unit tests
|
||||
# `npm test`, not a bare `node --test`: the bare form also collects
|
||||
# `scripts/blind-test.mjs` (it matches `**/*-test.mjs`) and records its
|
||||
# usage error as a failing test, so CI went red while every local command
|
||||
# looked green.
|
||||
run: npm test
|
||||
|
||||
- name: Prompt contract lint
|
||||
run: node scripts/prompt-lint.mjs
|
||||
|
||||
- name: Skill template freshness
|
||||
run: node scripts/generate-template.mjs --check
|
||||
|
||||
# Every demand of the v2 objective, mapped to the artefact that proves it.
|
||||
# Acceptance itself runs in the next job, so this is scope-only.
|
||||
- name: Objective audit
|
||||
run: node scripts/audit-objective.mjs --skip-acceptance
|
||||
|
||||
# Release hygiene: versions, schema marker, carried directories, gates.
|
||||
- name: Release check
|
||||
run: node scripts/check_release.mjs
|
||||
|
||||
acceptance:
|
||||
name: Acceptance (public corpus)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22"
|
||||
|
||||
# The visual-check phase drives a real browser; playwright is a dev
|
||||
# dependency and never ships with the package.
|
||||
- name: Install Playwright
|
||||
run: |
|
||||
npm install --no-save playwright@1.62.1
|
||||
npx playwright install --with-deps chromium
|
||||
|
||||
- name: End-to-end acceptance on the bundled corpus
|
||||
run: node scripts/acceptance.mjs --evidence "$RUNNER_TEMP/evidence"
|
||||
|
||||
# The second public corpus is multi-source (two chat exports + a document)
|
||||
# and dated, so the same phases run on a corpus the derivation was designed for.
|
||||
- name: End-to-end acceptance on the multi-source corpus
|
||||
run: node scripts/acceptance.mjs --corpus tests/fixtures/public-corpus/synthetic-multisource --evidence "$RUNNER_TEMP/evidence-multisource"
|
||||
|
||||
# Scope audit *with* the acceptance run it normally performs.
|
||||
- name: Objective audit (includes acceptance)
|
||||
run: DISTILLY_PLAYWRIGHT_ROOT="$PWD" node scripts/audit-objective.mjs
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: acceptance-evidence
|
||||
path: |
|
||||
${{ runner.temp }}/evidence
|
||||
${{ runner.temp }}/evidence-multisource
|
||||
if-no-files-found: ignore
|
||||
|
||||
@@ -20,6 +20,8 @@ Distilly is a local-first product for turning a person's source material, workin
|
||||
|
||||
## 这一支(`dot-skill-test`):人物 Skill + 证据脊柱
|
||||
|
||||
栈停在 `dot-skill-test`,`dot-skill` 保留。
|
||||
|
||||
**把一个人的原材料蒸馏成一个可调用的人物 Skill,外加一份每条结论都能回指到原文的画像页。**
|
||||
零运行时依赖,只要 Node ≥ 20。
|
||||
|
||||
@@ -33,7 +35,7 @@ Distilly is a local-first product for turning a person's source material, workin
|
||||
|
||||
**这份实现与那条 Plugin 路线的区别,一句话**:它不追求"像不像",它保证"凭什么这么说"——
|
||||
每条结论都能回指到原文的字节区间(`[k00NN]` 锚点),派生可复跑(同一输入两次字节相同),
|
||||
交付物由机械门禁压住(验收 17 项,含"产物齐 / Layer 0–5 齐 / 悬空锚点=0")。
|
||||
交付物由机械门禁压住(验收 18 项,含"产物齐 / Layer 0–5 齐 / 悬空锚点=0")。
|
||||
|
||||
### 装到一个宿主(以 DeepSeek Harness 为例)
|
||||
|
||||
@@ -50,9 +52,9 @@ node bin/distilly.mjs install deepseek-harness # → $DSH_HOME/skills/distil
|
||||
### 怎么验
|
||||
|
||||
```bash
|
||||
npm test # 391 项,Node 20 与 22 各一遍
|
||||
node scripts/acceptance.mjs --corpus tests/fixtures/public-corpus/synthetic-interview --person lin-gong # 17/17
|
||||
DISTILLY_PLAYWRIGHT_ROOT=<含 node_modules 的目录> node scripts/audit-objective.mjs # 15/15
|
||||
npm test # 397 项,Node 20 与 22 各一遍
|
||||
node scripts/acceptance.mjs --corpus tests/fixtures/public-corpus/synthetic-interview --person lin-gong # 18/18
|
||||
DISTILLY_PLAYWRIGHT_ROOT=<含 node_modules 的目录> node scripts/audit-objective.mjs --skip-acceptance # 14/14
|
||||
```
|
||||
|
||||
**当前状态、已知缺口、分支与 PR 清单**:[`docs/v2/STATUS.md`](docs/v2/STATUS.md)。
|
||||
|
||||
+13
-1
@@ -19,7 +19,9 @@ Anyone can become a reusable `.skill` for AI agents and compatible bots.
|
||||
|
||||
<br>
|
||||
|
||||
*Last updated: 2026-08-23*
|
||||
*Last updated: 2026-09-22*
|
||||
|
||||
栈停在 `dot-skill-test`,`dot-skill` 保留。
|
||||
|
||||
[**Chinese**](docs/lang/ROADMAP_ZH.md) · [**Spanish**](docs/lang/ROADMAP_ES.md) · [**German**](docs/lang/ROADMAP_DE.md) · [**Japanese**](docs/lang/ROADMAP_JA.md) · [**Russian**](docs/lang/ROADMAP_RU.md) · [**Portuguese**](docs/lang/ROADMAP_PT.md) · [**Korean**](docs/lang/ROADMAP_KO.md)
|
||||
|
||||
@@ -27,6 +29,16 @@ Anyone can become a reusable `.skill` for AI agents and compatible bots.
|
||||
|
||||
---
|
||||
|
||||
## Distilly v2 CLI (this tree)
|
||||
|
||||
栈停在 `dot-skill-test`,`dot-skill` 保留。
|
||||
|
||||
These CONTRACT clauses are not shipped. They live here instead of the frozen command surface.
|
||||
|
||||
- PDF extraction. `parse-doc` refuses `.pdf` with exit 2 and `office/unsupported-format`. Register extracted text with `note --from`.
|
||||
- HTML axe (WCAG 2.2 A/AA) and internal broken-link checks on rendered pages.
|
||||
- `python3 tools/*.py` forwarding. The CLI does not wrap the old Python tools.
|
||||
|
||||
## What's Done (v1.0)
|
||||
|
||||
| Capability | Status |
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
> 描述的是按会话转录重建的 `ds/NN-*` 分支快照,不是本栈。
|
||||
|
||||
# PR-01 · Node 单栈基座:入口 CLI + Skill 内核 + 安装器 + 拼音 + 测试移植
|
||||
|
||||
- 分支:`ds/01-node-core`(12 个提交,已本地合并进 `dot-skill-test`)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
> 描述的是按会话转录重建的 `ds/NN-*` 分支快照,不是本栈。
|
||||
|
||||
# PR-02 · 零凭据解析:`knowledge/` 账本 + 锚点 + chat/subtitle/archive 解析
|
||||
|
||||
- 分支:`ds/02-parse-zero-cred`(6 个提交,已本地合并进 `dot-skill-test`)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
> 描述的是按会话转录重建的 `ds/NN-*` 分支快照,不是本栈。
|
||||
|
||||
# PR-03 渲染层证据(`ds/03-render`)
|
||||
|
||||
纯文字证据。截图/PNG **不入库**,见 §6;本文件只记录数值、命令与结果。
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
> 描述的是按会话转录重建的 `ds/NN-*` 分支快照,不是本栈。
|
||||
|
||||
# PR-04 · 提示词层(`ds/04-prompts`)证据
|
||||
|
||||
纯文字 PR,无 HTML 产物、无截图(`/tmp/dst-evidence/` 无文件)。命令名一律以 `docs/v2/CONTRACT.md` §1 的命令块为准。
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
> 描述的是按会话转录重建的 `ds/NN-*` 分支快照,不是本栈。
|
||||
|
||||
# PR-05 · coding-agent 适配层:矩阵文档与防漂移断言
|
||||
|
||||
- 分支:`ds/05-agents`(基于 `dot-skill-test`)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
> 描述的是按会话转录重建的 `ds/NN-*` 分支快照,不是本栈。
|
||||
|
||||
# PR-06 · `retrospect`:确定性回望派生
|
||||
|
||||
- 分支:`ds/06-retrospect`(基于 `dot-skill-test`)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
> 描述的是按会话转录重建的 `ds/NN-*` 分支快照,不是本栈。
|
||||
|
||||
# PR-07 · 需要凭据的采集渠道 + computer-use 同意门(ds/07-collect-consent)
|
||||
|
||||
契约:`docs/v2/CONTRACT.md`(§1 命令表、§2 磁盘契约、§3 回执与密钥、§4 证据纪律、§5 门禁)。
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
> 描述的是按会话转录重建的 `ds/NN-*` 分支快照,不是本栈。
|
||||
|
||||
# PR-09 · 证据脊柱打通 + 效果层盲测装置
|
||||
|
||||
- 分支:`dot-skill-test`(集成分支,本轮 9 个原子提交直接在本地集成分支上)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
> 描述的是按会话转录重建的 `ds/NN-*` 分支快照,不是本栈。
|
||||
|
||||
# PR-10 · 效果层盲测:实跑两轮的结果与结论
|
||||
|
||||
- 目标:按 `docs/v2/ACCEPTANCE.md` 的 A/B holdout 真跑一次"证据层 vs 裸 prompt",
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
> 描述的是按会话转录重建的 `ds/NN-*` 分支快照,不是本栈。
|
||||
|
||||
# PR-11 · 归因修复:派生层第一次认得出「谁」+ 飞书导出解析
|
||||
|
||||
- 分支:`ds/10-attribution`(3 个原子提交)已本地合并进 `dot-skill-test`;飞书导出在
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
> 描述的是按会话转录重建的 `ds/NN-*` 分支快照,不是本栈。
|
||||
|
||||
# PR-12 · 飞书三路补齐 + `note`:CONTRACT §1 命令面完成
|
||||
|
||||
- 分支:`ds/11-feishu-clients`(3 个原子提交)与 `ds/12-note`(1 个),都已本地合并进 `dot-skill-test`
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
> 描述的是按会话转录重建的 `ds/NN-*` 分支快照,不是本栈。
|
||||
|
||||
# PR-13 · 目标审计门禁
|
||||
|
||||
- 分支:直接落在集成分支 `dot-skill-test`(本地)
|
||||
@@ -44,7 +46,7 @@ node --test tests/audit-objective.test.mjs # 2 条:全体满足 + 缺
|
||||
- `docs/evidence/pr-01-node-core.md`、`pr-02-parse-zero-cred.md`:最早两条分支缺证据文档,
|
||||
审计把它标成唯一未满足项后补写。
|
||||
- 契约里未移植的四个采集渠道(discord/reddit/notion/gmail)从"读起来像拼错"改成
|
||||
`collect/planned-channel` + 逐条需求说明,`doctor` 多打一行 `Planned channels`。
|
||||
当时用 planned-channel 这类错误码做缺口说明,并让 doctor 多打一行 Planned channels。那是 ds/13 快照上的历史行为,不是本栈。
|
||||
|
||||
## 6. 已知缺口
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
> 描述的是按会话转录重建的 `ds/NN-*` 分支快照,不是本栈。
|
||||
|
||||
# PR-15 · 跨渠道身份映射(`identity.json`)
|
||||
|
||||
- 分支:`ds/15-identity`(本地,合并进 `dot-skill-test`)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
> 描述的是按会话转录重建的 `ds/NN-*` 分支快照,不是本栈。
|
||||
|
||||
# PR-16 · Discord 与 Notion 两个采集渠道
|
||||
|
||||
- 分支:`ds/16-discord-notion`(本地,已合并进 `dot-skill-test`)
|
||||
@@ -8,7 +10,7 @@
|
||||
## 1. 为什么
|
||||
|
||||
`CONTRACT.md` §1 的采集命令行写着 8 个渠道,此前只实现 4 个(飞书/Slack/钉钉/X)。
|
||||
未实现的两个渠道此前回 `collect/planned-channel` 并写明所需凭据——诚实,但仍是缺口。
|
||||
未实现的两个渠道当时用 planned-channel 这类错误码回执并写明所需凭据。那是 ds/16 快照上的历史说明,不是本栈。
|
||||
这一轮把 Discord(bot token)与 Notion(内部集成 token)补上,`PENDING_CHANNELS`
|
||||
从 4 条缩到 2 条(reddit / gmail)。
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
> 描述的是按会话转录重建的 `ds/NN-*` 分支快照,不是本栈。
|
||||
|
||||
# PR-17 · Reddit 与 Gmail:契约的采集渠道全部实现
|
||||
|
||||
- 分支:`ds/17-reddit-gmail`(本地,已合并进 `dot-skill-test`)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
> 描述的是按会话转录重建的 `ds/NN-*` 分支快照,不是本栈。
|
||||
|
||||
# PR-18 · 池化口径收尾 + 带身份映射的第四次盲测
|
||||
|
||||
- 分支:`dot-skill-test`(本地集成分支)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
> 描述的是按会话转录重建的 `ds/NN-*` 分支快照,不是本栈。
|
||||
|
||||
# PR-19 · 迁移把旧内容导入证据脊柱 + 发布检查
|
||||
|
||||
- 分支:`dot-skill-test`(本地集成分支)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
> 描述的是按会话转录重建的 `ds/NN-*` 分支快照,不是本栈。
|
||||
|
||||
# PR-20 · 仓库忽略规则:把用户数据、凭据与本地证据的边界补回来
|
||||
|
||||
- 分支:`ds/20-repo-hygiene`(worktree `/tmp/dot-skill-test-20`,已本地合并进 `dot-skill-test`)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
> 描述的是按会话转录重建的 `ds/NN-*` 分支快照,不是本栈。
|
||||
|
||||
# PR-21 · 发布产物与 CI 测试命令:两个「门禁全绿但东西是坏的」缺陷
|
||||
|
||||
- 分支:`ds/21-package-ci`(worktree `/tmp/dot-skill-test-21`,已本地合并进 `dot-skill-test`)
|
||||
|
||||
+3
-5
@@ -19,22 +19,21 @@ harvest <dir|file> 零凭据:目录/文件 → knowledge/
|
||||
parse-chat <export.json> ChatGPT / Claude / Slack 导出 / Telegram / Discord
|
||||
parse-email <x.eml|.mbox>
|
||||
parse-subtitle <x.srt|.vtt>
|
||||
parse-doc <x.docx|xlsx|pdf>
|
||||
parse-doc <x.docx|xlsx>
|
||||
parse-archive <x.zip|dir> X 官方归档 / Takeout / Discord / Telegram / Instagram / Facebook / LinkedIn
|
||||
retrospect 纯派生 → evidence/derived/*.json(跑两次字节相同)
|
||||
collect <feishu|slack|dingtalk|x|discord|reddit|notion|gmail> 要 key / OAuth
|
||||
collect x --mode browser --consent <token> computer use(无 token → exit 2)
|
||||
transcribe <audio|video> 可选后端(OpenAI 兼容 HTTP 或宿主能力)
|
||||
note --from <file|-> 把 LLM 自己读到的内容登记进账本(method:"model-read")
|
||||
consent <grant|list|revoke> ~/.distilly/consent.json
|
||||
consent <grant|list|verify|revoke|prune> ~/.distilly/consent.json
|
||||
view check | view render [--shareable]
|
||||
doctor 证据覆盖率 / 不可用渠道 / 锚点回指率 / computer-use 占比
|
||||
skill <create|update|list|version>
|
||||
skill migrate
|
||||
install <host> | uninstall
|
||||
```
|
||||
|
||||
迁移期兼容:旧的 `python3 tools/xxx.py` 调用由 `bin/distilly.mjs` 转发并打印 deprecation 警告(PR① 内实现,PR③ 删除)。
|
||||
|
||||
## 2. 磁盘契约
|
||||
|
||||
```
|
||||
@@ -80,7 +79,6 @@ skills/<family>/<slug>/
|
||||
- 确定性:同一输入**跑两次 sha256 相同**(派生类命令)。
|
||||
- 锚点完整性:`docs`/`views` 里引用的每个锚点必须能在 `knowledge/index.json` 回指。
|
||||
- prompt 契约 lint:命令名必须真实存在、双语两段一致、锚点格式统一、禁止明文密钥字样。
|
||||
- HTML 产物:内部链接 0 坏链 + axe(WCAG 2.2 A/AA)0 violations(serious/critical)。
|
||||
|
||||
## 6. 双语
|
||||
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
# 门禁
|
||||
|
||||
`tests/gates/*.json` 各定义一条门禁。文件名等于 `id`。`scripts/run-gates.mjs` 读这个目录,用 `sh -c` 跑 `command`,按 `pass` 里的字面谓词判定。
|
||||
|
||||
谓词可以是:
|
||||
|
||||
- 退出码
|
||||
- stdout 包含某行
|
||||
- stdout 匹配某正则
|
||||
- ratchet
|
||||
|
||||
ratchet 从输出里抽失败名。`parser` 为 `tap` 时抽 `not ok N - <name>`。`parser` 为 `acceptance` 时抽 `❌ <phase>`。抽到的失败名与基线文件比对。新失败与过期基线行都算 FAIL。基线为空时再套 `final` 谓词。
|
||||
|
||||
基线文件在 `tests/gates/baseline/`。首行是注释。其余每一行是一条已知失败。基线只能缩小。修掉一条失败就删掉对应那一行。不要加行。不要重排留下的行。
|
||||
|
||||
本树不是叠好的栈顶。仍红的行必须留下。
|
||||
|
||||
## 怎么加一条门禁
|
||||
|
||||
1. 在 `tests/gates/` 新建 `<id>.json`。
|
||||
2. `id` 必须等于文件名去掉 `.json`。
|
||||
3. 写 `group`、`command`、`pass`。需要浏览器的加 `"requires": ["playwright"]`。
|
||||
4. 若起步时已有已知失败,写 `tests/gates/baseline/<id>.txt`,并在 `pass.ratchet` 里指向它。
|
||||
5. 跑 `node scripts/run-gates.mjs --id <id>`。新失败必须进基线,否则门禁红。
|
||||
6. 跑 `node scripts/run-gates.mjs --list --json` 确认新 id 出现。
|
||||
|
||||
## 怎么缩小基线
|
||||
|
||||
1. 先让对应测试由红转绿。
|
||||
2. 从基线文件删掉那一行。只删真正变绿或关掉的行。
|
||||
3. 再跑 `node scripts/run-gates.mjs --id <id>`。若出现 `stale baseline line, delete it`,说明删对了还漏了一行,或删了仍红的行。
|
||||
4. 不要用 `--write-baseline` 把仍红的失败冲掉。
|
||||
|
||||
## 常用命令
|
||||
|
||||
```
|
||||
node scripts/run-gates.mjs --list
|
||||
node scripts/run-gates.mjs --list --json
|
||||
node scripts/run-gates.mjs --without playwright
|
||||
DISTILLY_PLAYWRIGHT_ROOT=/tmp/pw-root node scripts/run-gates.mjs --requires playwright
|
||||
node scripts/run-gates.mjs --id audit-tests
|
||||
node scripts/check-entry-surface.mjs --baseline tests/gates/baseline/entry-surface.txt
|
||||
```
|
||||
|
||||
Node 20 与 Node 22 都要跑 `--without playwright`。playwright 组只在 Node 22 跑。仓库里不执行 `npm install`。
|
||||
+60
-53
@@ -1,10 +1,11 @@
|
||||
# dot-skill v2 · 当前状态
|
||||
|
||||
> 这份文件是**这一支的权威状态说明**:现在能做什么、怎么验、哪些还没做到。
|
||||
> 最后更新:2026-09-15(集成分支 `dot-skill-test`)。
|
||||
> 最后更新:2026-09-22(集成分支 `dot-skill-test`)。栈停在 `dot-skill-test`,`dot-skill` 保留。
|
||||
> 契约 `docs/v2/CONTRACT.md` · 验收 `docs/v2/ACCEPTANCE.md` · 渲染 `docs/v2/RENDER.md` ·
|
||||
> 提示词 `docs/v2/PROMPTS.md` · 宿主矩阵 `docs/v2/HOSTS.md` · 迁移 `docs/v2/MIGRATION.md`。
|
||||
> 各功能的交付说明与前后对比在 `docs/evidence/pr-NN-*.md`。
|
||||
> 提示词 `docs/v2/PROMPTS.md` · 宿主矩阵 `docs/v2/HOSTS.md` · 迁移 `docs/v2/MIGRATION.md` ·
|
||||
> 门禁 `docs/v2/GATES.md`。
|
||||
> 各功能的交付说明与前后对比在 `docs/evidence/pr-NN-*.md`。那些文件描述的是按会话转录重建的 `ds/NN-*` 分支快照,不是本栈。
|
||||
|
||||
## 1. 这是什么
|
||||
|
||||
@@ -52,29 +53,36 @@ node bin/distilly.mjs install deepseek-harness # → $DSH_HOME/skills/disti
|
||||
用于直接指名更细的一层;`view` 另接受同义别名 `--root`。同时给 `--base-dir` 与
|
||||
`--skills-dir` 会报错,不会猜。
|
||||
|
||||
本树 `doctor` 不接受 `--person`。空 HOME 探测必须同时把 `HOME` 和 `DISTILLY_HOME` 指到同一个空的 `mktemp -d`。
|
||||
|
||||
## 4. 怎么验(当前实测数字)
|
||||
|
||||
下列数字来自 2026-09-22 在 `atomic/pr-10-contract-align` 上的重跑。Node 22.14.0 与 Node 20.20.2。不要抄计划里的 396。
|
||||
|
||||
```bash
|
||||
npm test # CI 的同一条命令
|
||||
npm test # Verify 里 unit-tests 门禁的同一条命令
|
||||
node scripts/acceptance.mjs --corpus tests/fixtures/public-corpus/synthetic-interview --person lin-gong
|
||||
node scripts/acceptance.mjs --corpus tests/fixtures/public-corpus/us-house-floor-2009-07-29 --person us-house
|
||||
DISTILLY_PLAYWRIGHT_ROOT=<含 node_modules 的目录> node scripts/audit-objective.mjs
|
||||
node scripts/acceptance.mjs --corpus tests/fixtures/public-corpus/synthetic-multisource --person lin-gong
|
||||
node scripts/audit-objective.mjs --skip-acceptance
|
||||
node scripts/check_release.mjs && node scripts/prompt-lint.mjs && node scripts/generate-template.mjs --check
|
||||
node scripts/run-gates.mjs --without playwright
|
||||
DISTILLY_PLAYWRIGHT_ROOT=/tmp/pw-root node scripts/run-gates.mjs --requires playwright
|
||||
```
|
||||
|
||||
| 门禁 | 结果 |
|
||||
| --- | --- |
|
||||
| `npm test` | 391 / 391(Node 20 与 22 各一遍) |
|
||||
| `scripts/acceptance.mjs` | **18 / 18**,两份公开语料各一遍 |
|
||||
| `scripts/audit-objective.mjs` | 15 / 15 条满足(本地要求工作树干净且已推送) |
|
||||
| `scripts/check_release.mjs` | 7 / 7 |
|
||||
| `scripts/prompt-lint.mjs` | 0 finding / 26 文件 |
|
||||
| `scripts/generate-template.mjs --check` | 无漂移 |
|
||||
| GitHub Actions | Node 20 / Node 22 / Acceptance 三个 job 全绿 |
|
||||
|
||||
> 2026-09-15 补上的两道门禁:**语料体检**(`doctor --require-shape`:单元数、可归属比例、
|
||||
> 最活跃者占比,FAIL 必给理由——它拦住的正是"拿议会记录蒸出会议室的画像"那类错误)与
|
||||
> **引用判定**(`doctor` 现在把生成物里的引用一并计入,悬空引用 → `ok:false`、exit 1)。
|
||||
| 门禁 | 结果 | 来源命令 |
|
||||
| --- | --- | --- |
|
||||
| `npm test` | **397 / 397**,`# fail 0`(Node 20.20.2 与 22.14.0 各一遍) | `npm test` |
|
||||
| `scripts/acceptance.mjs` | **18 / 18**,三份公开语料各一遍 | `DISTILLY_PLAYWRIGHT_ROOT=/tmp/pw-root node scripts/run-gates.mjs --requires playwright` |
|
||||
| us-house 形状 | `FAIL · units=2744 speakers=18 attributed=175` | 同上,acceptance-us-house |
|
||||
| `scripts/audit-objective.mjs --skip-acceptance` | **14 / 14** 条满足 | `node scripts/audit-objective.mjs --skip-acceptance` |
|
||||
| `scripts/check_release.mjs` | **7 / 7** 项通过 | `node scripts/check_release.mjs` |
|
||||
| `scripts/prompt-lint.mjs` | `prompt-lint: 0 finding(s) in 0 file(s) across 26 file(s) scanned (contract commands: 16)` | `node scripts/prompt-lint.mjs` |
|
||||
| `scripts/generate-template.mjs --check` | 无漂移 | `node scripts/generate-template.mjs --check` |
|
||||
| `run-gates --without playwright` | **8 / 8** gates passed(Node 20 与 22) | `node scripts/run-gates.mjs --without playwright` |
|
||||
| `run-gates --requires playwright` | **3 / 3** gates passed | `DISTILLY_PLAYWRIGHT_ROOT=/tmp/pw-root node scripts/run-gates.mjs --requires playwright` |
|
||||
| `check-entry-surface --baseline` | `RESULT PASS findings 9 known 9 new 0 stale-baseline 0` | `node scripts/check-entry-surface.mjs --baseline tests/gates/baseline/entry-surface.txt` |
|
||||
| GitHub Actions | `verify.yml` 两个 job。`.github/workflows/ci.yml` 已删 | `git ls-files .github/workflows` |
|
||||
|
||||
验收 18 项里最后 6 项是**交付物与语料门禁**(判据在 `scripts/skill-artifacts.mjs`,纯字符串入参、
|
||||
可单测、带反例):产物齐 / PART A·B·运行规则在 / Layer 0–5 齐且 Layer 0 有规则 /
|
||||
@@ -84,54 +92,53 @@ node scripts/check_release.mjs && node scripts/prompt-lint.mjs && node scripts/g
|
||||
|
||||
产品要的是**一个人的一手产出**:本人著作、演讲与访谈字幕、本人社媒。
|
||||
会议记录与多人群聊是**混合流水**,必须先有说话人归属才能切出一个人——解析层现在能做到
|
||||
(WebVTT voice / `Name:` 前缀 / 全角冒号 `说话人 1:` / 句中换人),但归属率取决于源本身:
|
||||
真实 C-SPAN 字幕 47 分钟里只有 84 处说话人标签,解析层取到 82 条,覆盖率约 6%。
|
||||
(WebVTT voice / `Name:` 前缀 / 全角冒号 `说话人 1:` / 句中换人),但归属率取决于源本身。
|
||||
|
||||
**结论:选对语料比优化解析器重要一个数量级。** 产品现在会在 Step 1.5 拦住错误形状
|
||||
(`distilly doctor --require-shape`),上面这份 C-SPAN 语料的实测判决是
|
||||
`FAIL units=2744 speakers=18 attributed=175(3%)`,理由写明"先补这个人自己的一手产出"。
|
||||
(`distilly doctor --require-shape`)。本树 C-SPAN 语料的实测判决字面值是
|
||||
`FAIL · units=2744 speakers=18 attributed=175`。不要用 85 或 86。本树没有把 PR-1 merge 进来。
|
||||
|
||||
合成访谈字幕是 **38** 条 cue。见 `tests/fixtures/public-corpus/README.md`。
|
||||
|
||||
## 6. 分支与 PR 清单
|
||||
|
||||
栈停在 `dot-skill-test`,`dot-skill` 保留。程序不改 `dot-skill`。不更新、不 retarget、不合并 #185。
|
||||
|
||||
本文件描述的对象是 `dot-skill-test` 上的原子 PR 栈,外加本 PR 以 PR-9 为父提交的工作树。本树**没有**把 PR-1 至 PR-8 merge 进来。A8:不干净且不打回的父 PR 不挡住本条。
|
||||
|
||||
| 对象 | 数量 | 状态 |
|
||||
| --- | --- | --- |
|
||||
| 本地 `ds/NN-*` 分支 | 23 | 每条都有 `docs/evidence/pr-NN-*.md`(审计第 11 行按分支全量检查) |
|
||||
| 子功能 PR `#166`–`#184` | 19 | **堆叠**(base 是上一条 `ds/*` 分支);见下方三组状态 |
|
||||
| 集成分支 PR `#185` | 1 | 目标 `dot-skill`;CI 绿 |
|
||||
| 集成分支 | `dot-skill-test` | 本文件描述的对象 |
|
||||
| 原子 PR 栈 PR-0 至 PR-9 | 10 条已发布 | 见下表。总 verdict 多数不干净,A5 不打回 |
|
||||
| 本 PR-10 | 1 | 父提交 `atomic/pr-9-node-installers` @ `bb6d43110933ff106573ab9a5eb07f95f4d38ce4`(#187) |
|
||||
| 旧 `ds/NN-*` 快照与 #166 至 #184 | 仍开着 | D4 未执行。证据文档顶部已标明不是本栈 |
|
||||
| #185 | 1 | 不碰 |
|
||||
| 默认分支 `dot-skill` | 保留 | 程序任何环节都不改它 |
|
||||
|
||||
子功能 PR 的三组状态(2026-09-15 首次真正跑 CI 之后):
|
||||
已发布的原子 PR(2026-09-22 程序状态表):
|
||||
|
||||
| 组 | 分支 | CI | 说明 |
|
||||
| PR | GitHub | head SHA | 父 |
|
||||
| --- | --- | --- | --- |
|
||||
| **A. 可评审** | `ds/13` `ds/15` `ds/16` `ds/17` `ds/18` `ds/19` `ds/20` `ds/21`(8 条) | **全绿**(Node 20 / Node 22 / Acceptance) | 已并入集成分支上的 3 个修复提交(Node 20 的 `npm test`、审计推送行、测试读真实 home / 大小写断言) |
|
||||
| **B. 一条结构性红** | `ds/12-note`(1 条) | 4 红 2 绿 | 补了同样 3 个修复后仍剩 1 条:该分支的 `check_release` 要求 `docs/v2/IDENTITY.md`,而那是 `ds/15` 才交付的文件 —— 重建快照混了时代 |
|
||||
| **C. 快照不自洽** | `ds/01`–`ds/11`(10 条) | 30–79 红 | 分支的树与它自带的测试对不上(例:`ds/01` 只有 20 个测试文件,集成分支有 86 个),**不作为合并入口**;功能已包含在 `dot-skill-test` 与后续分支里 |
|
||||
| PR-0 | #186 | `9c8f1ceb464988f4273b2c5fc4bfc4bbe33af3ec` | `dot-skill-test` @ e16cc8a |
|
||||
| PR-1 | #188 | `319e8c36633313450ba0b6c2695c55e39b835085` | PR-0 |
|
||||
| PR-9 | #187 | `bb6d43110933ff106573ab9a5eb07f95f4d38ce4` | PR-0。**本 PR 的父提交** |
|
||||
| PR-4 | #189 | `cd99139b9d2e2770616c58f899b76bdf75be474c` | 未合进本树 |
|
||||
| PR-6 | #190 | `bc809f0eba1424625887d700aa395f2f3f595d25` | 未合进本树 |
|
||||
| PR-5 | #191 | `009415e163357ce57143ac18ddfd1738c6ee4d5f` | 未合进本树 |
|
||||
| PR-2 | #192 | `8784cac5bd15b47ecc1d9511eee637d77d8baf20` | 未合进本树 |
|
||||
| PR-3 | #193 | `d05717bbefc2278e1cc55ca74745b359cea9cfdd` | 未合进本树 |
|
||||
| PR-8 | #194 | `2b8b22fde9bd41fe9e6ab5d854efd2e91b2da4d8` | 未合进本树 |
|
||||
| PR-7 | #195 | `57ffcbff84e26d74696cf94613e6154e9903b4ca` | 未合进本树 |
|
||||
|
||||
原始 7 项任务(`ds/01`–`ds/07`)的交付与验证方式见各自的 `docs/evidence/pr-NN-*.md`。
|
||||
`docs/evidence/pr-NN-*.md` 描述的是按会话转录重建的 `ds/NN-*` 分支快照,不是本栈。
|
||||
|
||||
## 7. 已知缺口(没做到的部分,按严重度)
|
||||
|
||||
1. **效果数字不可用 —— 装置已就绪,只差一个"不是蒸馏者的"裁判**。
|
||||
上一次真实结果(`docs/evidence/pr-10-blind-test-runs.md`):run 2 evidence 命中率 0.563 /
|
||||
control 0.500,**四臂 0 编造**;只有 10 条 cue / 10 条断言,差异与裁判方差同量级。
|
||||
2026-09-15 已在**右形状语料**(synthetic-interview,76 单元)上重跑机械部分并交付一个盲测包:
|
||||
A/B 按时间码切好(28 / 10 条)、实验组页面已定稿(私有模式校验:A 段 24 条原句 **0 条**
|
||||
出现在页面里,25 个锚点)、对照组裸 prompt 画像已写、裁判题与评分模板齐备。
|
||||
**剩下的两步必须由另一个模型或人来完成**:裁判(写 10 行「特征 | 预测」)与核对者(唯一读 B 的角色)。
|
||||
本机只注册了一个模型(`deepseek-official/deepseek-flash`),同一模型既蒸馏又裁判的数字没有意义。
|
||||
包的位置与确切命令见随包 `README.md`(`/tmp/dst-evidence/blind-2026-09-15/`,本地证据不入库)。
|
||||
2. **Step 3(Read)没有机械门禁**,全靠提示词自觉;Step 1 / 1.5 / 2 / 4 / 5 有判据。
|
||||
3. **10 个 `ds/*` PR 的树与它们自带的测试不自洽**(`ds/01`–`ds/11`):CI 30–79 红,
|
||||
是快照本身不完整(例:`ds/01` 只有 20 个测试文件,集成分支有 86 个)。
|
||||
其中 9 条已并入集成分支的修复、8 条 CI 转绿、`ds/12` 剩 1 条结构性失败(见第 6 节的分组表);
|
||||
剩下这 10 条**不打算修**——它们是重建时的不完整快照,功能已包含在集成分支里。
|
||||
4. **19 个 PR 的 base 结构与流程要求不符**:要求是"PR 到 `dot-skill-test`",实际是堆叠到
|
||||
上一条 `ds/*` 分支(`ds/01` 的 base 甚至是 `pre-v2-baseline`)。CI 触发名单已扩到
|
||||
`ds/**` 并推到各分支,所以现在跑得起来;base 结构未改。
|
||||
1. **本树不是叠好的栈顶(A8)**。计划里「60 条全绿、两个基线只剩注释、entry-surface 0 finding」以叠好的栈为前提。本 PR 只删本 PR 真正变绿的基线行。`audit-tests` 基线仍有 20 行。`entry-surface` 基线仍有 9 行(四条 enumerations、四条 channels、一条 unknown-channel 文档缺口)。CONTRACT 第 25 行渠道名单未改。
|
||||
2. **仍红的审计行不在本 PR**。留下的包括 R08、R16、R17、R21、R24、R28、R38、R48、R49、R56。它们属于 PR-1 至 PR-8。不要删这些行来凑「只剩注释」。
|
||||
3. **Step 3(Read)没有机械门禁**,全靠提示词自觉;Step 1 / 1.5 / 2 / 4 / 5 有判据。
|
||||
4. **效果数字需要不是蒸馏者的裁判**。PR-8 的三模型 Lane 7 与 Lane 8 本拍未作为本树的前置。装置在 `scripts/blind-test.mjs`。
|
||||
5. **`docs/PRD.md` 与 `docs/SKILL_TYPE_ABSTRACTION_DESIGN*.md` 描述的是 v1(Python)形状**
|
||||
(`colleagues/{slug}`、`tools/*.py`),未随 v2 重写;两份文件顶部已加说明指向 v2。
|
||||
6. **`docs/evidence/pr-NN-*.md` 是各功能当时的历史证据**:其中 `pr-21` 与 `pr-13` 的结论
|
||||
已被后续工作推翻,两份文件末尾都补了更正章节;其余文件的数字仍然成立。
|
||||
7. **`README.md` 已分区**:本分支(Skill + CLI)在前,`distilly-plugin` 那条 Plugin 线
|
||||
明确划到「另一条产品线」标题下并注明不在本分支运行。
|
||||
6. **D4 未执行**。#166 至 #184 一条都不关。证据头不写「已关闭」。
|
||||
7. **PDF 抽取、HTML axe 与坏链、python3 转发**已从契约挪到 `ROADMAP.md`。`parse-doc` 对 `.pdf` 响亮拒绝,code 为 `office/unsupported-format`。
|
||||
8. **四个契约渠道 discord / reddit / notion / gmail 在本树不可达**。那是 PR-4 的 Files。本 PR 不删模块,不改 CONTRACT 第 25 行。
|
||||
|
||||
@@ -37,12 +37,19 @@ const git = (...args) => execFileSync("git", args, { cwd: root, encoding: "utf8"
|
||||
{
|
||||
const tracked = git("ls-files").split("\n");
|
||||
const python = tracked.filter((path) => /\.py$/.test(path) || /(^|\/)requirements\.txt$/.test(path));
|
||||
const ci = readFileSync(join(root, ".github", "workflows", "ci.yml"), "utf8");
|
||||
const pythonCi = /python-version|setup-python|pip install/.test(ci);
|
||||
const verify = readFileSync(join(root, ".github", "workflows", "verify.yml"), "utf8");
|
||||
const pythonCi = /python-version|setup-python|pip install/.test(verify);
|
||||
const listed = JSON.parse(
|
||||
execFileSync(process.execPath, [join(root, "scripts", "run-gates.mjs"), "--list", "--json"], { cwd: root, encoding: "utf8" }),
|
||||
);
|
||||
const ids = listed.map((gate) => gate.id);
|
||||
const nodeGates = ["unit-tests", "syntax-check"].every((id) => ids.includes(id));
|
||||
record(
|
||||
"Node 单栈:仓库里没有 Python / requirements.txt,CI 只跑 Node",
|
||||
python.length === 0 && !pythonCi,
|
||||
python.length === 0 ? `0 tracked .py files; CI jobs: ${(ci.match(/^\s{2}[a-z-]+:$/gm) ?? []).map((line) => line.trim()).join(" ")}` : `still tracked: ${python.slice(0, 5).join(", ")}`,
|
||||
python.length === 0 && !pythonCi && nodeGates,
|
||||
python.length === 0
|
||||
? `0 tracked .py files; verify.yml Python steps: ${pythonCi}; run-gates ids include unit-tests/syntax-check: ${nodeGates}`
|
||||
: `still tracked: ${python.slice(0, 5).join(", ")}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+101
-35
@@ -13,8 +13,9 @@
|
||||
* tag names the same version the package declares.
|
||||
*/
|
||||
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { execFileSync, spawnSync } from "node:child_process";
|
||||
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join, resolve } from "node:path";
|
||||
|
||||
import { SCHEMA_VERSION } from "../src/skill/schema.mjs";
|
||||
@@ -24,24 +25,43 @@ const root = resolve(import.meta.dirname, "..");
|
||||
const json = process.argv.includes("--json");
|
||||
const tagIndex = process.argv.indexOf("--tag");
|
||||
const tag = tagIndex === -1 ? null : process.argv[tagIndex + 1];
|
||||
const bin = join(root, "bin", "distilly.mjs");
|
||||
|
||||
const rows = [];
|
||||
const record = (name, ok, evidence) => rows.push({ name, ok: Boolean(ok), evidence });
|
||||
|
||||
const read = (relative) => readFileSync(join(root, relative), "utf8");
|
||||
const pkg = JSON.parse(read("package.json"));
|
||||
const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
|
||||
|
||||
function parseJson(text) {
|
||||
const objectStart = text.indexOf("{");
|
||||
const arrayStart = text.indexOf("[");
|
||||
const start =
|
||||
arrayStart >= 0 && (objectStart < 0 || arrayStart < objectStart) ? arrayStart : objectStart;
|
||||
if (start < 0) throw new SyntaxError("no JSON in command output");
|
||||
return JSON.parse(text.slice(start));
|
||||
}
|
||||
|
||||
/* 1 — one version, everywhere it is printed ---------------------------------- */
|
||||
{
|
||||
const binVersion = execFileSync(process.execPath, [join(root, "bin", "distilly.mjs"), "--version"], { encoding: "utf8" }).trim();
|
||||
const skill = existsSync(join(root, "SKILL.md")) ? read("SKILL.md") : "";
|
||||
const skillVersion = /^version:\s*"?([^"\n]+)"?/m.exec(skill)?.[1]?.trim() ?? null;
|
||||
const consistent = binVersion === pkg.version && (skillVersion === null || skillVersion === pkg.version);
|
||||
record(
|
||||
"版本一致(package.json / --version / SKILL.md)",
|
||||
consistent,
|
||||
`package.json ${pkg.version}, --version ${binVersion}, SKILL.md ${skillVersion ?? "(未声明)"}`,
|
||||
);
|
||||
const binVersion = execFileSync(process.execPath, [bin, "--version"], { encoding: "utf8" }).trim();
|
||||
const work = mkdtempSync(join(tmpdir(), "rel-skill-"));
|
||||
try {
|
||||
execFileSync(
|
||||
process.execPath,
|
||||
[bin, "skill", "create", "--character", "colleague", "--name", "ReleaseCheck", "--slug", "release-check", "--base-dir", work, "--json"],
|
||||
{ encoding: "utf8", cwd: root },
|
||||
);
|
||||
const meta = JSON.parse(readFileSync(join(work, "skills", "colleague", "release-check", "meta.json"), "utf8"));
|
||||
const productVersion = meta.version;
|
||||
const consistent = binVersion === pkg.version && productVersion !== undefined && String(productVersion).length > 0;
|
||||
record(
|
||||
"版本一致(--version 与 skill create 产物)",
|
||||
consistent,
|
||||
`package.json ${pkg.version}, --version ${binVersion}, skill create meta.version ${productVersion}`,
|
||||
);
|
||||
} finally {
|
||||
rmSync(work, { recursive: true, force: true });
|
||||
}
|
||||
if (tag !== null) {
|
||||
record("发布 tag 指向同一版本", tag === `v${pkg.version}` || tag === pkg.version, `--tag ${tag} vs ${pkg.version}`);
|
||||
}
|
||||
@@ -49,24 +69,64 @@ const pkg = JSON.parse(read("package.json"));
|
||||
|
||||
/* 2 — schemas are the frozen ones, and a migration exists -------------------- */
|
||||
{
|
||||
const migrate = existsSync(join(root, "src", "skill", "migrate.mjs"));
|
||||
const migrationTest = read(join("tests", "schema-migration.test.mjs"));
|
||||
record(
|
||||
`schema v${SCHEMA_VERSION} 与账本 v${LEDGER_SCHEMA_VERSION},迁移脚本在`,
|
||||
SCHEMA_VERSION === "4" && migrate && /idempot/i.test(migrationTest),
|
||||
`SCHEMA_VERSION=${SCHEMA_VERSION}, LEDGER_SCHEMA_VERSION=${LEDGER_SCHEMA_VERSION}, src/skill/migrate.mjs=${migrate}, 幂等断言=${/idempot/i.test(migrationTest)}`,
|
||||
);
|
||||
const testRun = spawnSync(process.execPath, ["--test", join(root, "tests", "schema-migration.test.mjs")], {
|
||||
cwd: root,
|
||||
encoding: "utf8",
|
||||
});
|
||||
const work = mkdtempSync(join(tmpdir(), "rel-migrate-"));
|
||||
try {
|
||||
const skillDir = join(work, "skills", "colleague", "old-person");
|
||||
mkdirSync(skillDir, { recursive: true });
|
||||
writeFileSync(join(skillDir, "SKILL.md"), "---\nname: 张三\n---\n\n# 张三\n", "utf8");
|
||||
writeFileSync(join(skillDir, "persona.md"), "# 张三\n", "utf8");
|
||||
writeFileSync(join(skillDir, "work.md"), "# 工作\n", "utf8");
|
||||
writeFileSync(
|
||||
join(skillDir, "meta.json"),
|
||||
JSON.stringify({ name: "张三", slug: "old-person", character: "colleague", schema_version: "3", lifecycle: { version: "v1" } }, null, 2),
|
||||
"utf8",
|
||||
);
|
||||
writeFileSync(join(skillDir, "manifest.json"), JSON.stringify({ slug: "old-person", schema_version: "3", artifacts: {} }, null, 2), "utf8");
|
||||
execFileSync(process.execPath, [bin, "skill", "migrate", "--base-dir", work, "--json"], { encoding: "utf8", cwd: root });
|
||||
const second = parseJson(execFileSync(process.execPath, [bin, "skill", "migrate", "--base-dir", work, "--json"], { encoding: "utf8", cwd: root }));
|
||||
const third = parseJson(execFileSync(process.execPath, [bin, "skill", "migrate", "--base-dir", work, "--json"], { encoding: "utf8", cwd: root }));
|
||||
const same = JSON.stringify(second) === JSON.stringify(third);
|
||||
record(
|
||||
`schema v${SCHEMA_VERSION} 与账本 v${LEDGER_SCHEMA_VERSION},两次 migrate 回执相同`,
|
||||
SCHEMA_VERSION === "4" && testRun.status === 0 && same,
|
||||
`SCHEMA_VERSION=${SCHEMA_VERSION}, LEDGER_SCHEMA_VERSION=${LEDGER_SCHEMA_VERSION}, schema-migration.test status=${testRun.status}, receipts-equal=${same}`,
|
||||
);
|
||||
} finally {
|
||||
rmSync(work, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
/* 3 — the installers still carry the evidence spine -------------------------- */
|
||||
{
|
||||
const hosts = read(join("src", "install", "hosts.mjs"));
|
||||
const carried = ["knowledge/raw", "knowledge/text", "evidence", "views"].every((name) => hosts.includes(name));
|
||||
record(
|
||||
"安装器携带 evidence spine(knowledge/raw、knowledge/text、evidence、views)",
|
||||
carried,
|
||||
carried ? "CARRIED_DIRECTORIES 覆盖四项" : "CARRIED_DIRECTORIES 缺项",
|
||||
);
|
||||
const destRoot = mkdtempSync(join(tmpdir(), "rel-install-"));
|
||||
const dest = join(destRoot, "distilly");
|
||||
const personRoot = mkdtempSync(join(tmpdir(), "rel-spine-"));
|
||||
try {
|
||||
execFileSync(process.execPath, [bin, "install", "--path", dest, "--json"], { encoding: "utf8", cwd: root });
|
||||
const skillDir = join(personRoot, "skills", "colleague", "old-person");
|
||||
mkdirSync(skillDir, { recursive: true });
|
||||
writeFileSync(join(skillDir, "SKILL.md"), "---\nname: x\n---\n", "utf8");
|
||||
writeFileSync(join(skillDir, "meta.json"), JSON.stringify({ schema_version: "3", slug: "old-person" }), "utf8");
|
||||
execFileSync(process.execPath, [join(dest, "bin", "distilly.mjs"), "skill", "migrate", "--base-dir", personRoot, "--json"], {
|
||||
encoding: "utf8",
|
||||
cwd: root,
|
||||
});
|
||||
const four = ["knowledge/raw", "knowledge/text", "evidence", "views"];
|
||||
const missing = four.filter((name) => !existsSync(join(skillDir, name)));
|
||||
const listing = readdirSync(skillDir).join(", ");
|
||||
record(
|
||||
"安装器携带 evidence spine(knowledge/raw、knowledge/text、evidence、views)",
|
||||
missing.length === 0,
|
||||
missing.length === 0 ? `install --path then ls: ${four.join(", ")} under migrated skill; skill dir: ${listing}` : `missing after install --path: ${missing.join(", ")}`,
|
||||
);
|
||||
} finally {
|
||||
rmSync(destRoot, { recursive: true, force: true });
|
||||
rmSync(personRoot, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
/* 4 — the package is zero-dependency and Python-free ------------------------- */
|
||||
@@ -94,14 +154,21 @@ const pkg = JSON.parse(read("package.json"));
|
||||
|
||||
/* 6 — the gates a release claims are runnable -------------------------------- */
|
||||
{
|
||||
const gates = ["scripts/acceptance.mjs", "scripts/audit-objective.mjs", "scripts/prompt-lint.mjs", "scripts/visual-check.mjs", "scripts/split-corpus.mjs", "scripts/blind-test.mjs"];
|
||||
const missing = gates.filter((path) => !existsSync(join(root, path)));
|
||||
const ci = read(join(".github", "workflows", "ci.yml"));
|
||||
const wired = ["node --test", "acceptance.mjs", "prompt-lint.mjs", "audit-objective.mjs"].every((needle) => ci.includes(needle));
|
||||
const listed = parseJson(execFileSync(process.execPath, [join(root, "scripts", "run-gates.mjs"), "--list", "--json"], { encoding: "utf8", cwd: root }));
|
||||
const ids = listed.map((gate) => gate.id);
|
||||
const required = [
|
||||
"unit-tests",
|
||||
"acceptance-synthetic-interview",
|
||||
"acceptance-us-house",
|
||||
"acceptance-multisource",
|
||||
"prompt-lint",
|
||||
"objective-audit",
|
||||
];
|
||||
const missing = required.filter((id) => !ids.includes(id));
|
||||
record(
|
||||
"发布所依赖的门禁都在,且 CI 会跑",
|
||||
missing.length === 0 && wired,
|
||||
`missing: ${missing.length === 0 ? "none" : missing.join(", ")}; CI 覆盖 node --test / acceptance / prompt-lint / audit: ${wired}`,
|
||||
"发布所依赖的门禁都在,且 Verify 会跑",
|
||||
missing.length === 0,
|
||||
missing.length === 0 ? `run-gates --list --json ids include ${required.join(", ")}` : `missing gate ids: ${missing.join(", ")}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -121,4 +188,3 @@ if (json) {
|
||||
console.log(`\n${rows.length - failed.length}/${rows.length} 项通过${failed.length === 0 ? "" : `;未通过:${failed.map((row) => row.name).join(";")}`}`);
|
||||
}
|
||||
process.exit(failed.length === 0 ? 0 : 1);
|
||||
|
||||
|
||||
+13
-15
@@ -19,8 +19,8 @@
|
||||
*
|
||||
* Names may contain one space (`view check`, `skill create`); dispatch prefers
|
||||
* the two-token name. Commands promised by `docs/v2/CONTRACT.md` §1 but not yet
|
||||
* implemented are listed in `PLANNED` together with the branch that owns them,
|
||||
* so an unfinished build reports "not implemented" instead of "unknown command".
|
||||
* registered live in `PLANNED`. An empty map means the CLI reports unknown
|
||||
* command rather than "not implemented". Remaining product work is in ROADMAP.md.
|
||||
*/
|
||||
|
||||
import { CliError } from "../cli/receipt.mjs";
|
||||
@@ -37,12 +37,10 @@ function registry() {
|
||||
return REGISTRY;
|
||||
}
|
||||
|
||||
/** Commands frozen in CONTRACT §1 whose implementation ships in another branch. */
|
||||
/** Commands frozen in CONTRACT §1 that this CLI has not registered. */
|
||||
export const PLANNED = {
|
||||
// Contract §1 is fully implemented on this branch (the command surface, the
|
||||
// eight collection channels, note, view, skill migrate). The map stays because
|
||||
// `missingCommandError` and `doctor` read it to explain what is *not* here;
|
||||
// it being empty is the signal that nothing is outstanding.
|
||||
// Empty: every CONTRACT §1 command this CLI registers is present. Product
|
||||
// work that is not a registered command lives in ROADMAP.md, not this map.
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -138,17 +136,17 @@ export function padDisplay(text, width) {
|
||||
}
|
||||
|
||||
const CATALOG_ZH = [
|
||||
["skill create|update|list|version", "创建 / 更新 / 列出 / 归档 Skill(本分支)"],
|
||||
["install <host>", "把 Distilly 装进某个宿主的 skills 目录(本分支)"],
|
||||
["uninstall [<host>|--path]", "卸载已安装的 Distilly(本分支)"],
|
||||
["doctor", "宿主与 Skill 库存体检(本分支,最小版)"],
|
||||
["skill create|update|list|version|migrate", "创建 / 更新 / 列出 / 归档 / 迁移 Skill"],
|
||||
["install <host>", "把 Distilly 装进某个宿主的 skills 目录"],
|
||||
["uninstall [<host>|--path]", "卸载已安装的 Distilly"],
|
||||
["doctor", "宿主与 Skill 库存体检"],
|
||||
];
|
||||
|
||||
const CATALOG_EN = [
|
||||
["skill create|update|list|version", "create / update / list / archive Skills (this branch)"],
|
||||
["install <host>", "install Distilly into a host skills directory (this branch)"],
|
||||
["uninstall [<host>|--path]", "remove an installed Distilly (this branch)"],
|
||||
["doctor", "host and skill inventory health check (this branch, minimal)"],
|
||||
["skill create|update|list|version|migrate", "create / update / list / archive / migrate Skills"],
|
||||
["install <host>", "install Distilly into a host skills directory"],
|
||||
["uninstall [<host>|--path]", "remove an installed Distilly"],
|
||||
["doctor", "host and skill inventory health check"],
|
||||
];
|
||||
|
||||
/** Bilingual help for the whole CLI (CONTRACT §6: 中文 → `---` → English). */
|
||||
|
||||
@@ -52,7 +52,7 @@ function parseArgs(argv) {
|
||||
}
|
||||
|
||||
register("parse-archive", {
|
||||
summary: "读取 docx/xlsx / read OOXML documents into knowledge/",
|
||||
summary: "读取归档 / read X Takeout Discord Telegram archives into knowledge/",
|
||||
usage: "distilly parse-archive <file...> --person <slug> [--base-dir <dir>] [--source <label>] [--json]",
|
||||
...help,
|
||||
run({ argv, json, reporter }) {
|
||||
|
||||
@@ -73,6 +73,28 @@ register("parse-doc", {
|
||||
const { options, paths } = parsed;
|
||||
if (!options.person) return fail("--person is required", "parse-doc/usage");
|
||||
|
||||
const pdf = paths.find((target) => resolve(target).toLowerCase().endsWith(".pdf"));
|
||||
if (pdf) {
|
||||
return {
|
||||
receipt: {
|
||||
command: "parse-doc",
|
||||
person: options.person,
|
||||
ok: false,
|
||||
inputs: [],
|
||||
outputs: [],
|
||||
anchors: { total: 0, cited: 0 },
|
||||
warnings: [],
|
||||
unavailable: [],
|
||||
error: {
|
||||
code: "office/unsupported-format",
|
||||
message: `${pdf}: PDF is not supported`,
|
||||
remedy: "extract the text and register it with `distilly note --from <file|->`",
|
||||
},
|
||||
},
|
||||
exitCode: 2,
|
||||
};
|
||||
}
|
||||
|
||||
const personDir = resolve(options.baseDir, "skills", "colleague", options.person);
|
||||
const store = new KnowledgeStore(personDir);
|
||||
const ledger = loadLedger(store);
|
||||
|
||||
@@ -137,6 +137,11 @@ function paragraphRecords(paragraphs, kind = "paragraph") {
|
||||
/** Dispatch on the file name; anything else is refused by name. */
|
||||
export function parseOffice(file, options = {}) {
|
||||
const name = file.name.toLowerCase();
|
||||
if (name.endsWith(".pdf")) {
|
||||
const error = new Error(`${file.label}: PDF is not supported; register extracted text with note --from`);
|
||||
error.code = "office/unsupported-format";
|
||||
throw error;
|
||||
}
|
||||
if (name.endsWith(".docx")) return parseDocx(file, options);
|
||||
if (name.endsWith(".xlsx")) return parseXlsx(file, options);
|
||||
throw new Error(`${file.label}: not an OOXML document (.docx/.xlsx)`);
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
||||
import { existsSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { distilly, fixtures, readJson, receipt, repoRoot, workspace } from "./helpers.mjs";
|
||||
@@ -185,11 +185,25 @@ test("R10 an unknown command exits 1 with code unknown-command and a remedy", ()
|
||||
|
||||
test("R23 `parse-doc` advertises pdf alongside docx and xlsx (CONTRACT §1 line 22)", () => {
|
||||
const ws = workspace();
|
||||
const run = distilly(["parse-doc", "--help"], { cwd: ws.root, env: { HOME: ws.home } });
|
||||
assert.equal(run.status, 0);
|
||||
assert.match(run.stdout, /docx/);
|
||||
assert.match(run.stdout, /xlsx/);
|
||||
assert.match(run.stdout, /pdf/i, "CONTRACT §1 promises `parse-doc <x.docx|xlsx|pdf>`");
|
||||
const helpRun = distilly(["parse-doc", "--help"], { cwd: ws.root, env: { HOME: ws.home } });
|
||||
assert.equal(helpRun.status, 0);
|
||||
assert.match(helpRun.stdout, /docx/);
|
||||
assert.match(helpRun.stdout, /xlsx/);
|
||||
assert.equal(/pdf/i.test(helpRun.stdout), false, "parse-doc --help must not promise pdf");
|
||||
|
||||
const pdf = join(ws.root, "fake.pdf");
|
||||
writeFileSync(pdf, "%PDF-1.4\n");
|
||||
const before = new Set(readdirSync(ws.root, { recursive: true }));
|
||||
const run = distilly(["parse-doc", pdf, "--person", "p", "--base-dir", ws.root, "--json"], {
|
||||
cwd: ws.root,
|
||||
env: { HOME: ws.home, DISTILLY_HOME: ws.distillyHome },
|
||||
});
|
||||
assert.equal(run.status, 2, run.stderr);
|
||||
const body = receipt(run);
|
||||
assert.equal(body.error?.code, "office/unsupported-format");
|
||||
assert.match(String(body.error?.remedy ?? ""), /note --from/);
|
||||
const after = new Set(readdirSync(ws.root, { recursive: true }));
|
||||
assert.deepEqual([...after].sort(), [...before].sort());
|
||||
});
|
||||
|
||||
test("R12/R58 zero runtime dependencies, engines.node >= 20, and no Python in the tree", () => {
|
||||
|
||||
+5
-1
@@ -17,8 +17,12 @@ public-corpus/
|
||||
|
||||
## 当前夹具
|
||||
|
||||
数字来自本树对文件的计数。`transcript.srt` 里单独一行的十进制序号当作一条 cue。
|
||||
|
||||
| 目录 | 内容 | 许可 |
|
||||
| --- | --- | --- |
|
||||
| `synthetic-interview/` | 我们自写的合成访谈(后端工程师 × 技术面试官,45 条字幕,含时间跨度、语气变化、一处回避话题) | 本项目原创,随仓库以 MIT 发布;**不是真人对话**,仅用于工程验收 |
|
||||
| `synthetic-interview/` | 合成访谈(后端工程师 × 技术面试官)。`transcript.srt` **38** 条 cue。含时间跨度、语气变化、一处回避话题。 | 本项目原创,随仓库以 MIT 发布;**不是真人对话**,仅用于工程验收 |
|
||||
| `synthetic-multisource/` | 三来源合成材料:飞书页、Slack 消息、事后文档 | 本项目原创,随仓库以 MIT 发布 |
|
||||
| `us-house-floor-2009-07-29/` | 真实 C-SPAN 众议院发言字幕。`transcript.srt` **1346** 条 cue | 见同目录 `LICENSE.md` |
|
||||
|
||||
> 合成语料能验证"流程正确"(锚点、字节守恒、确定性、渲染、溢出),但**不能**验证"蒸出来的角色讨不讨喜"——那需要真实素材,见 `docs/v2/ACCEPTANCE.md`。
|
||||
|
||||
@@ -5,7 +5,6 @@ R08 `collect reddit --json` is a known channel (CONTRACT §1 line 25), so its fa
|
||||
R08 `collect notion --json` is a known channel (CONTRACT §1 line 25), so its failure is about credentials, not about the channel name
|
||||
R08 `collect gmail --json` is a known channel (CONTRACT §1 line 25), so its failure is about credentials, not about the channel name
|
||||
R08 every collection module under src/collect/ is reachable from bin/distilly.mjs (module present ≠ command wired)
|
||||
R23 `parse-doc` advertises pdf alongside docx and xlsx (CONTRACT §1 line 22)
|
||||
R08 `collect feishu --mode browser` (SKILL.md 迁移对照表) reaches the browser route and asks for consent
|
||||
R08 `collect feishu --mode mcp` (SKILL.md 迁移对照表: feishu_mcp_client.py → collect feishu) reaches the MCP route
|
||||
R08 a contract channel that is not shipped is reported as planned (PR \#185 正文: collect/planned-channel), never as an unknown channel
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
# Known entry-surface findings. Remove a line when its PR closes the gap. Never add one without a PR that explains it.
|
||||
commands: +skill migrate
|
||||
enumerations: -collect: discord
|
||||
enumerations: -collect: reddit
|
||||
enumerations: -collect: notion
|
||||
enumerations: -collect: gmail
|
||||
enumerations: +consent: verify
|
||||
enumerations: +consent: prune
|
||||
enumerations: -parse-doc: pdf
|
||||
channels: -collect discord: collect/unknown-channel
|
||||
channels: -collect reddit: collect/unknown-channel
|
||||
channels: -collect notion: collect/unknown-channel
|
||||
channels: -collect gmail: collect/unknown-channel
|
||||
error-codes: -collect/planned-channel documented but no source emits it
|
||||
error-codes: +collect/unknown-channel observed at runtime but documented nowhere
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { existsSync, mkdtempSync, readdirSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { fileURLToPath } from "node:url";
|
||||
@@ -133,7 +134,7 @@ test("these fixtures never write through to the repository manifest", () => {
|
||||
assert.equal(readFileSync(manifestPath, "utf8"), before, "the repository manifest must be untouched");
|
||||
});
|
||||
|
||||
test("`npm test` and CI run the same command, and every path it names exists", () => {
|
||||
test("`npm test` and Verify run the same unit gate, and every path it names exists", () => {
|
||||
// A bare `node --test` also collects `scripts/blind-test.mjs` (`**/*-test.mjs`)
|
||||
// and records it as a passing "test" — which is how CI went red while every local
|
||||
// command looked green.
|
||||
@@ -148,8 +149,19 @@ test("`npm test` and CI run the same command, and every path it names exists", (
|
||||
// Node 20 receives real paths (as it requires), while Node 22 — which treats a
|
||||
// positional argument as a glob of its own and refuses a bare directory with
|
||||
// `Cannot find module '…/tests'` — receives paths it can also handle.
|
||||
const ci = readFileSync(path.join(root, ".github", "workflows", "ci.yml"), "utf8");
|
||||
assert.match(ci, /^\s*run: npm test\s*$/m, "CI must invoke `npm test`");
|
||||
assert.equal(existsSync(path.join(root, ".github", "workflows", "ci.yml")), false, "ci.yml must be gone");
|
||||
const verify = readFileSync(path.join(root, ".github", "workflows", "verify.yml"), "utf8");
|
||||
assert.match(verify, /node scripts\/run-gates\.mjs --without playwright/, "Verify must invoke run-gates --without playwright");
|
||||
|
||||
const listed = spawnSync(process.execPath, [path.join(root, "scripts", "run-gates.mjs"), "--list", "--json"], {
|
||||
cwd: root,
|
||||
encoding: "utf8",
|
||||
});
|
||||
assert.equal(listed.status, 0, listed.stderr);
|
||||
const ids = JSON.parse(listed.stdout).map((gate) => gate.id);
|
||||
for (const id of ["unit-tests", "acceptance-synthetic-interview", "acceptance-us-house", "acceptance-multisource", "prompt-lint", "objective-audit"]) {
|
||||
assert.ok(ids.includes(id), `run-gates --list --json must include ${id}`);
|
||||
}
|
||||
|
||||
const argv = manifest.scripts.test.split(/\s+/);
|
||||
assert.equal(argv[0], "node");
|
||||
@@ -165,20 +177,20 @@ test("`npm test` and CI run the same command, and every path it names exists", (
|
||||
}
|
||||
});
|
||||
|
||||
test("`engines` matches what CI actually tests", () => {
|
||||
const ci = readFileSync(path.join(root, ".github", "workflows", "ci.yml"), "utf8");
|
||||
const matrix = /node-version:\s*\[([^\]]+)\]/.exec(ci)?.[1] ?? "";
|
||||
test("`engines` matches what Verify actually tests", () => {
|
||||
const verify = readFileSync(path.join(root, ".github", "workflows", "verify.yml"), "utf8");
|
||||
const matrix = /node:\s*\[([^\]]+)\]/.exec(verify)?.[1] ?? "";
|
||||
const versions = matrix.split(",").map((entry) => entry.trim().replace(/"/g, ""));
|
||||
const minimum = Number(manifest.engines.node.replace(/[^\d]/g, ""));
|
||||
assert.ok(versions.length > 0, "CI must declare a node-version matrix");
|
||||
assert.ok(versions.length > 0, "Verify must declare a node matrix");
|
||||
for (const version of versions) {
|
||||
assert.ok(
|
||||
Number(version) >= minimum,
|
||||
`CI tests Node ${version} but the package claims to need >= ${minimum}`,
|
||||
`Verify tests Node ${version} but the package claims to need >= ${minimum}`,
|
||||
);
|
||||
}
|
||||
assert.ok(
|
||||
versions.includes(String(minimum)),
|
||||
`the oldest supported Node (${minimum}) is not in the CI matrix, so the floor is untested`,
|
||||
`the oldest supported Node (${minimum}) is not in the Verify matrix, so the floor is untested`,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -35,3 +35,13 @@ test("a mismatched release tag fails the run", () => {
|
||||
assert.equal(tagRow.ok, false);
|
||||
assert.match(tagRow.evidence, /v9\.9\.9/);
|
||||
});
|
||||
|
||||
test("release check criteria are executable, not text-grep", () => {
|
||||
const result = spawnSync(process.execPath, [join(HERE, "scripts", "check-entry-surface.mjs")], {
|
||||
cwd: HERE,
|
||||
encoding: "utf8",
|
||||
});
|
||||
const block = result.stdout.slice(result.stdout.indexOf("release check criteria"));
|
||||
assert.ok(block.length > 0, "entry-surface must print release check criteria");
|
||||
assert.equal(/text-grep/.test(block), false, block);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user