فهرست منبع

feat(v7): M0 仓库骨架——v7/ npm 包 + node:test + 双矩阵 CI + Node 版本门槛

v7 第一批代码,骨架与脚手架,无业务逻辑(M1+ 在此长真实代码)。

- v7/package.json:name=webnovel-writer、ESM、engines.node>=22.13.0、零运行时依赖
- bin/webnovel-writer.js:版本门槛先行 + 子命令分发(init/update 占位,不崩溃)
- src/runtime/node-version.js:唯一真实逻辑——纯函数版本比较,<22.13.0 给中文人话提示
- src/{installer,state-machine,mechanical-check,prep,finalize,cache,storage}:职责占位模块
- test/:node:test 内置,版本门槛断言 + 中文路径 UTF-8 往返占位用例
- .github/workflows/v7-ci.yml:[ubuntu,windows] × [22.13.0, lts/*],仅触发 v7
- directory-structure.md:§2 落点 v7/、§4 包内布局回填,基线 1.0→1.1 / spec 0.6→0.8

本机验证:node --test 4 绿、bin 冒烟正确、零第三方 import。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
lingfengQAQ 22 ساعت پیش
والد
کامیت
c53bf63daf

+ 28 - 0
.github/workflows/v7-ci.yml

@@ -0,0 +1,28 @@
+name: v7 CI
+
+on:
+  push:
+    branches: [v7]
+  pull_request:
+    branches: [v7]
+
+jobs:
+  test:
+    strategy:
+      fail-fast: false
+      matrix:
+        os: [ubuntu-latest, windows-latest]
+        node: ['22.13.0', 'lts/*']
+    runs-on: ${{ matrix.os }}
+    defaults:
+      run:
+        working-directory: v7
+    steps:
+      - uses: actions/checkout@v4
+      - uses: actions/setup-node@v4
+        with:
+          node-version: ${{ matrix.node }}
+      - name: 单元测试(含中文路径用例)
+        run: node --test
+      - name: 版本门槛冒烟
+        run: node bin/webnovel-writer.js --version

+ 28 - 7
.trellis/spec/backend/directory-structure.md

@@ -1,6 +1,6 @@
 # 目录结构规范
 
-> 版本:基线 1.0(2026-06-12)。依据:v7 PRD 1.0、story-repo-spec 0.6。v7 代码骨架落地后增量修订
+> 版本:基线 1.1(2026-06-27,M0 仓库骨架落地,回填 §2/§4)。依据:v7 PRD 1.0、story-repo-spec 0.8
 
 ---
 
@@ -18,12 +18,12 @@ webnovel-writer/                # 仓库根
 ├── .trellis/                   # 开发流程层(任务/规范/日志),与产品代码无关
 ├── webnovel-writer/            # v6 插件本体(遗产,禁止改动)
 ├── requirements.txt pytest.ini # v6 Python 遗产(禁止改动;v7 禁止引入 Python)
-└── (v7 代码目录待定)          # npx 包骨架落地时修订本节
+└── v7/                         # v7 产品代码(Node ESM 包,零依赖):package.json / bin / src / test
 ```
 
 2.1 **文档先行**:`docs/architecture/v7-prd.md` 是产品决策真源,两份 spec 是格式与多宿主行为真源。代码与文档冲突时以文档为准;变更行为必须先修订文档(走任务流程),再改代码。
 
-## 3. v7 产品代码布局原则(骨架待定,原则已定
+## 3. v7 产品代码布局原则(M0 骨架已落地,结构见 §4
 
 3.1 分发渠道只有 npx(`npx webnovel-writer init` / `update`),即单一 npm 包;禁止恢复插件市场分发。
 
@@ -36,8 +36,29 @@ webnovel-writer/                # 仓库根
 
 3.4 文件排序必须依赖零填充数字前缀(`0152-`、`第05卷`、`伏笔-031`),禁止依赖中文字典序。
 
-## 4. 待增量补充
+## 4. v7 包内布局(M0 落地)
 
-- [ ] npm 包内部 src/ 模块划分(安装器、状态机、机检、备料、定稿)——首个代码任务时定
-- [ ] 测试目录与命名约定
-- [ ] 题材模板与知识库在包内的存放位置
+源码根 `v7/`,ESM、零运行时依赖:
+
+```
+v7/
+├── package.json            # name=webnovel-writer, type=module, engines.node>=22.13.0, 零 deps
+├── bin/
+│   └── webnovel-writer.js  # CLI 入口:版本门槛先行 + 子命令分发(init/update 占位)
+├── src/
+│   ├── runtime/            # 版本门槛等运行时基础
+│   ├── installer/          # 安装器(M5)
+│   ├── state-machine/      # 状态机单入口(M3)
+│   ├── mechanical-check/   # 机检(M2)
+│   ├── prep/               # 备料(M2)
+│   ├── finalize/           # 定稿原子提交(M2)
+│   ├── cache/              # .cache/index.db,node:sqlite(M1,见 O4 缓存设计文档)
+│   └── storage/            # 存储适配器小端口(M1,spec §1.5)
+└── test/                   # node:test,*.test.js 与 src 镜像
+```
+
+4.1 测试:Node 内置 `node:test` + `node:assert`,**禁止第三方测试框架**;测试文件命名 `v7/test/**/*.test.js`,与 `src/` 镜像。
+
+4.2 模块为按职责(Use Case)划分,**非通用工具层**;端口拆小,禁止上帝对象(spec §1.5)。
+
+4.3 待补:题材模板与知识库在包内的存放位置(知识层平移任务时定)。

+ 34 - 0
v7/bin/webnovel-writer.js

@@ -0,0 +1,34 @@
+#!/usr/bin/env node
+// CLI 入口:版本门槛先行,再分发子命令。M0 子命令均为占位,不实现业务逻辑。
+import { readFileSync } from 'node:fs'
+import { checkNodeVersion } from '../src/runtime/node-version.js'
+
+const gate = checkNodeVersion(process.version)
+if (!gate.ok) {
+  process.stderr.write(gate.message + '\n')
+  process.exit(1)
+}
+
+const command = process.argv[2]
+switch (command) {
+  case '--version':
+  case '-v':
+    process.stdout.write(readPackageVersion() + '\n')
+    break
+  case 'init':
+  case 'update':
+    process.stdout.write(`「${command}」尚未实现(M0 骨架占位)。\n`)
+    break
+  case undefined:
+    process.stdout.write('用法:webnovel-writer <init|update>\n')
+    break
+  default:
+    process.stderr.write(`未知命令「${command}」。可用:init、update。\n`)
+    process.exit(1)
+}
+
+function readPackageVersion() {
+  const pkgUrl = new URL('../package.json', import.meta.url)
+  const pkg = JSON.parse(readFileSync(pkgUrl, 'utf8'))
+  return pkg.version
+}

+ 16 - 0
v7/package.json

@@ -0,0 +1,16 @@
+{
+  "name": "webnovel-writer",
+  "version": "0.0.0",
+  "description": "AI 网文写作工作流(v7 重写,M0 骨架)",
+  "type": "module",
+  "engines": {
+    "node": ">=22.13.0"
+  },
+  "bin": {
+    "webnovel-writer": "bin/webnovel-writer.js"
+  },
+  "scripts": {
+    "test": "node --test"
+  },
+  "dependencies": {}
+}

+ 3 - 0
v7/src/cache/index.js

@@ -0,0 +1,3 @@
+// 缓存:.cache/index.db(node:sqlite),首查重建,精准读取接口。见 O4 缓存设计文档。
+// 占位——真实实现见 M1。
+export {}

+ 3 - 0
v7/src/finalize/index.js

@@ -0,0 +1,3 @@
+// 定稿:原子 commit(正文入定稿、设定/时间线/名册更新、条目履历、章摘要、工作区清空)。
+// 占位——真实实现见 M2。
+export {}

+ 3 - 0
v7/src/installer/index.js

@@ -0,0 +1,3 @@
+// 安装器:npx webnovel-writer init / update,工作目录布局、平台壳生成、模板哈希追踪。
+// 占位——真实实现见 M5。
+export {}

+ 3 - 0
v7/src/mechanical-check/index.js

@@ -0,0 +1,3 @@
+// 机检:零 token 的可计数项(字数、禁词、复读、句式体检、新专名比对等)。
+// 占位——真实实现见 M2。
+export {}

+ 3 - 0
v7/src/prep/index.js

@@ -0,0 +1,3 @@
+// 备料:组装本章写作材料(全书近况 + 要写到的事 + 精准片段),默认精准读取。
+// 占位——真实实现见 M2。
+export {}

+ 41 - 0
v7/src/runtime/node-version.js

@@ -0,0 +1,41 @@
+// Node 版本门槛检查。
+// 纯函数与副作用分离:比较逻辑在此(可测),process.exit 留给入口 bin/。
+export const MIN_NODE = '22.13.0'
+
+/**
+ * 比较运行时 Node 版本是否满足最低要求。
+ * @param {string} versionString 形如 "v22.13.0" 或 "22.13.0"
+ * @returns {{ok: boolean, message: string}} ok=true 时 message 为空串
+ */
+export function checkNodeVersion(versionString) {
+  const current = parseVersion(versionString)
+  if (!current) {
+    return {
+      ok: false,
+      message: `无法识别 Node 版本「${versionString}」。本工具需要 Node ${MIN_NODE} 或更高版本。`,
+    }
+  }
+  if (compare(current, parseVersion(MIN_NODE)) >= 0) {
+    return { ok: true, message: '' }
+  }
+  return {
+    ok: false,
+    message:
+      `当前 Node 版本 ${current.join('.')} 过低,本工具需要 ${MIN_NODE} 或更高版本。\n` +
+      `请升级 Node 后重试:https://nodejs.org/(建议安装最新 LTS)。`,
+  }
+}
+
+function parseVersion(s) {
+  if (typeof s !== 'string') return null
+  const m = s.trim().replace(/^v/, '').match(/^(\d+)\.(\d+)\.(\d+)/)
+  if (!m) return null
+  return [Number(m[1]), Number(m[2]), Number(m[3])]
+}
+
+function compare(a, b) {
+  for (let i = 0; i < 3; i++) {
+    if (a[i] !== b[i]) return a[i] - b[i]
+  }
+  return 0
+}

+ 3 - 0
v7/src/state-machine/index.js

@@ -0,0 +1,3 @@
+// 状态机:单入口,启动序列与 7 个态的编排(只编排,不做业务判断)。
+// 占位——真实实现见 M3。
+export {}

+ 3 - 0
v7/src/storage/index.js

@@ -0,0 +1,3 @@
+// 存储适配器:容错读写 story repo 源文件的小端口(ChapterReader/Writer、ThreadLedger 等)。
+// 占位——真实实现见 M1(spec §1.5 架构原则:拆小端口,AI 只吃 DTO)。
+export {}

+ 22 - 0
v7/test/chinese-path.test.js

@@ -0,0 +1,22 @@
+import { test } from 'node:test'
+import assert from 'node:assert/strict'
+import { mkdtemp, mkdir, writeFile, readFile, rm } from 'node:fs/promises'
+import { tmpdir } from 'node:os'
+import { join } from 'node:path'
+
+// 守护不变量:中文目录名、中文文件名、中文内容在任何平台都必须 UTF-8 正确往返,
+// 不依赖系统 locale(Windows 中文环境是一等公民,story-repo-spec §2.2)。
+test('中文路径与中文内容 UTF-8 往返一致', async () => {
+  const base = await mkdtemp(join(tmpdir(), 'wnw-'))
+  try {
+    const dir = join(base, '测试书-第05卷')
+    await mkdir(dir, { recursive: true })
+    const file = join(dir, '伏笔-031-灭门真凶.md')
+    const content = '# 北境的雪\n林晚于北境得血书,玄阶令牌现世。\n'
+    await writeFile(file, content, { encoding: 'utf8' })
+    const readBack = await readFile(file, { encoding: 'utf8' })
+    assert.equal(readBack, content)
+  } finally {
+    await rm(base, { recursive: true, force: true })
+  }
+})

+ 28 - 0
v7/test/node-version.test.js

@@ -0,0 +1,28 @@
+import { test } from 'node:test'
+import assert from 'node:assert/strict'
+import { checkNodeVersion, MIN_NODE } from '../src/runtime/node-version.js'
+
+test('满足门槛的版本放行', () => {
+  for (const v of ['v22.13.0', 'v24.15.0', '22.13.1', 'v23.0.0']) {
+    const r = checkNodeVersion(v)
+    assert.equal(r.ok, true, `${v} 应放行`)
+    assert.equal(r.message, '')
+  }
+})
+
+test('低于门槛的版本拦截并给人话提示', () => {
+  for (const v of ['v22.12.0', 'v21.0.0', 'v18.20.0']) {
+    const r = checkNodeVersion(v)
+    assert.equal(r.ok, false, `${v} 应拦截`)
+    assert.ok(r.message.length > 0, '应有人话提示')
+    assert.ok(r.message.includes(MIN_NODE), '提示应含所需版本')
+  }
+})
+
+test('无法识别的版本串也拦截', () => {
+  for (const v of ['', 'abc', null, undefined]) {
+    const r = checkNodeVersion(v)
+    assert.equal(r.ok, false)
+    assert.ok(r.message.length > 0)
+  }
+})