diff --git a/build/README.md b/build/README.md index 16611fd..7ab75e8 100644 --- a/build/README.md +++ b/build/README.md @@ -60,6 +60,8 @@ powershell -ExecutionPolicy Bypass -File .\build\build-release.ps1 -OutputDirect ## 固定工具链与安装策略 +正式入口在安装依赖前以及构建结束时检查后端、前端受跟踪文件的实际换行,要求文本采用 LF,二进制和无换行文件不受影响。Git 干净状态可能忽略 CRLF/LF 差异,不能单独证明构建输入一致;发现 CRLF 或混合换行即拒绝发布,不自动改写源码。遇到此提示请从当前已提交版本重新克隆后构建,不要使用终端批量替换中文文件。检查结果及文件数量写入 manifest 的 build_environment.source_line_endings。 + `toolchain.mjs` 是正式发布入口调用的构建检查器,`toolchain.test.mjs` 是纳入前端测试命令的正式回归,不是临时验收脚本。 1. Go 精确版本以 `backend/go.mod` 为唯一来源;实际工具链不符则失败。`go.sum`、`go.mod` 固定 Go 依赖和数据库驱动,构建及归档使用 `-mod=readonly`;禁用外部 Go 工作区及 `GOFLAGS` 覆盖。 diff --git a/build/toolchain.mjs b/build/toolchain.mjs index 5500c9b..62b6fc4 100644 --- a/build/toolchain.mjs +++ b/build/toolchain.mjs @@ -119,6 +119,29 @@ export function inputHashes(backend, frontend) { ])); } +export function validateSourceLineEndings(output) { + const records = output.split('\0'); + requireCondition(records.pop() === '', '源码换行检查输出不完整。'); + requireCondition(records.length > 0, '源码目录没有受跟踪文件,不能确认构建输入。'); + const invalid = []; + for (const record of records) { + const match = /^i\/\S+\s+w\/(\S+)\s+attr\/[^\t]*\t([\s\S]+)$/.exec(record); + requireCondition(match, '无法解析源码换行检查结果。'); + const [, ending, path] = match; + requireCondition(['lf', 'crlf', 'mixed', 'none', '-text'].includes(ending), '源码换行检查返回未知文件状态。'); + if (ending === 'crlf' || ending === 'mixed') invalid.push(path); + } + requireCondition(invalid.length === 0, + `源码文件存在非规范换行,拒绝构建:${invalid.slice(0, 5).join('、')}。请从当前提交重新克隆后构建;工具不会改写源文件。`); + return { policy: 'lf', tracked_files: records.length }; +} + +export function inspectSourceLineEndings(directory) { + // Git 干净状态会忽略 CRLF/LF 差异;必须读取工作树的实际换行状态。 + const result = execute('git', ['ls-files', '--eol', '-z', '--', '.'], directory); + return validateSourceLineEndings(result.stdout); +} + export function installDependencies(frontend) { // 不信任使用者的 npm 全局/用户设置,命令行参数显式覆盖;保留构建所需的可选平台包。 const result = execute(npmCommand, [ @@ -132,6 +155,10 @@ export function installDependencies(frontend) { export function inspectToolchain(backend, frontend, install = false) { const host = { platform: platform(), arch: arch(), release: release(), version: version() }; validateEnvironment(process.env, host); + const sourceLineEndings = { + backend: inspectSourceLineEndings(backend), + frontend: inspectSourceLineEndings(frontend), + }; const pkg = JSON.parse(readFileSync(join(frontend, 'package.json'), 'utf8')); const lock = JSON.parse(readFileSync(join(frontend, 'package-lock.json'), 'utf8')); const npmrc = readFileSync(join(frontend, '.npmrc'), 'utf8'); @@ -153,6 +180,7 @@ export function inspectToolchain(backend, frontend, install = false) { dependency_scripts: 'disabled', dependency_install: install ? 'npm-ci' : 'not-run', input_sha256: hashes, + source_line_endings: sourceLineEndings, }; } diff --git a/build/toolchain.test.mjs b/build/toolchain.test.mjs index 67c54f1..e03f63a 100644 --- a/build/toolchain.test.mjs +++ b/build/toolchain.test.mjs @@ -5,11 +5,71 @@ import { tmpdir } from 'node:os'; import { dirname, join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import test from 'node:test'; -import { installDependencies, npmEnvironment, validateEnvironment, validateFrontend, validateGoVersion } from './toolchain.mjs'; +import { inspectSourceLineEndings, installDependencies, npmEnvironment, validateEnvironment, validateFrontend, validateGoVersion, validateSourceLineEndings } from './toolchain.mjs'; const root = resolve(dirname(fileURLToPath(import.meta.url)), '..'); const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm'; +test('源码换行检查接受规范文本、无换行文件及二进制', () => { + const records = [ + 'i/lf w/lf attr/text=auto eol=lf \t页面 空格.vue', + 'i/none w/none attr/text=auto eol=lf \tempty.js', + 'i/-text w/-text attr/text=auto eol=lf \timage.png', + 'i/lf w/lf attr/text=auto eol=lf \t换行\n文件.js', + ].join('\0') + '\0'; + assert.deepEqual(validateSourceLineEndings(records), { policy: 'lf', tracked_files: 4 }); +}); + +for (const ending of ['crlf', 'mixed']) { + test(`源码换行检查拒绝 ${ending} 而不是自动修改文件`, () => { + assert.throws(() => validateSourceLineEndings(`i/lf w/${ending} attr/text=auto eol=lf \t页面.vue\0`), /非规范换行.*页面.vue/); + }); +} + +test('源码换行检查拒绝空、截断、格式错误或未知状态', () => { + for (const output of ['', '\0', 'i/lf w/lf attr/text\tapp.js', 'invalid\0', 'i/lf w/unknown attr/text\tapp.js\0']) { + assert.throws(() => validateSourceLineEndings(output)); + } +}); + +test('实际 Git 干净差异仍可检测工作树换行,且不改写源文件', (t) => { + const directory = mkdtempSync(join(tmpdir(), 'scert-source-endings-')); + t.after(() => rmSync(directory, { recursive: true, force: true })); + const run = (...args) => { + const result = spawnSync('git', args, { cwd: directory, encoding: 'utf8', windowsHide: true }); + assert.equal(result.status, 0, `${result.error ?? ''}\n${result.stderr}`); + return result.stdout; + }; + run('init', '--quiet'); + writeFileSync(join(directory, '.gitattributes'), '* text=auto eol=lf\n'); + const source = join(directory, '页面.vue'); + writeFileSync(source, '\n'); + writeFileSync(join(directory, 'image.png'), Buffer.from([0, 13, 10, 255])); + run('add', '--', '.gitattributes', '页面.vue', 'image.png'); + assert.equal(inspectSourceLineEndings(directory).tracked_files, 3); + const changed = Buffer.from('\r\n'); + writeFileSync(source, changed); + assert.equal(run('diff', '--exit-code'), ''); + assert.throws(() => inspectSourceLineEndings(directory), /非规范换行/); + assert.deepEqual(readFileSync(source), changed); + const result = spawnSync(process.execPath, [join(root, 'build', 'toolchain.mjs'), + '--backend', directory, '--frontend', directory, '--install'], { + cwd: directory, encoding: 'utf8', windowsHide: true, + }); + assert.equal(result.status, 1); + assert.match(result.stderr, /非规范换行/); + assert.equal(result.stdout, ''); + assert.ok(!existsSync(join(directory, 'node_modules'))); + assert.ok(!existsSync(join(directory, 'package-lock.json'))); + assert.deepEqual(readFileSync(source), changed); +}); + +test('非 Git 目录不能跳过源码换行检查', (t) => { + const directory = mkdtempSync(join(tmpdir(), 'scert-source-no-git-')); + t.after(() => rmSync(directory, { recursive: true, force: true })); + assert.throws(() => inspectSourceLineEndings(directory), /git 执行失败/); +}); + function fixture() { const frontend = join(root, 'frontend'); const pkg = JSON.parse(readFileSync(join(frontend, 'package.json'), 'utf8')); diff --git a/doc/Linux生产流程测试记录.md b/doc/Linux生产流程测试记录.md index caeebb6..a4cfb70 100644 --- a/doc/Linux生产流程测试记录.md +++ b/doc/Linux生产流程测试记录.md @@ -1,4 +1,13 @@ # Linux 生产流程测试记录 +## 构建整改补充(2026-09-30 20:58) + +本批只修改正式构建工具与回归,不修改应用业务代码,也未重新部署。修改前检查点 70f19d0 已推送。 + +- 原工作区正式工具链拒绝非规范换行,发生在依赖安装之前,没有自动改写中文文件。规范独立克隆则通过检查,后端 547 个、前端 105 个受跟踪文件采用允许的换行状态。 +- 工具链 31 项、前端全量 237 项测试通过;新增真实 Git 夹具证明 Git diff 可忽略 CRLF,但正式入口仍退出 1,且不生成 node_modules 或锁文件。 +- 20:58 管理 API 返回 401,明确提示重新登录,确认此前浏览器会话已失效;公开管理入口和就绪检查仍正常。未把未认证接口的错误当成有效任务列表。 +- 后续需在规范克隆用本批提交完成正式发布构建。P0-03 目录边界复审结论不变;P0-04 整项及生产完整门禁仍未完成。 + ## 最新实机与复审记录(2026-09-30 20:53) ### 正式页面配置校验失败与恢复 diff --git a/doc/Scert整改任务执行清单.md b/doc/Scert整改任务执行清单.md index c6161a3..0e90b01 100644 --- a/doc/Scert整改任务执行清单.md +++ b/doc/Scert整改任务执行清单.md @@ -1,4 +1,12 @@ # Scert 整改任务执行清单 +## 构建换行整改任务更新(2026-09-30) + +- [x] 已实现拒绝非规范换行,不替换用户中文源码;记录后端、前端检查策略及文件数量。 +- [x] 正式回归覆盖 LF、无换行、二进制、中文和换行文件名、CRLF、混合换行、空输出、截断、无效格式、未知状态及非 Git 目录。 +- [x] 实际 Git 夹具验证:Git diff 无差异时仍能拒绝 CRLF;正式工具链退出 1,不安装依赖、不生成锁文件、不改写源码。 +- [x] 工具链 31 项和前端全量 237 项通过,规范克隆工具链检查通过。 +- [~] 提交后完整构建验证;Linux 原生构建、双平台服务、数据库与生产业务门禁仍未完成。 + ## 最新任务状态(2026-09-30 20:53) - [✓] P0-03 按本项范围完成复审,不再与整体生产验收混为一谈。源码与产物隔离、独立克隆构建、忽略规则、索引与实际部署包边界均有本轮证据。 diff --git a/doc/Scert整改合并优化清单.md b/doc/Scert整改合并优化清单.md index cd90229..a186040 100644 --- a/doc/Scert整改合并优化清单.md +++ b/doc/Scert整改合并优化清单.md @@ -1,4 +1,12 @@ # Scert 整改、合并与优化清单 +## 最新开发进度(2026-09-30,构建输入换行门禁) + +- [✓] P0-03 保持复审通过,证据见下节;不代表全部生产验收通过。 +- [x] P0-04 子项:正式工具链在安装依赖前及构建后读取实际源码换行,拒绝 CRLF、混合换行、无受跟踪源码及 Git 检查失败;不会自动改写文件。后端和前端检查结果写入发布清单。 +- [x] 工具链 31 项、前端全量 237 项测试通过;实际非规范工作区已被正式入口拒绝;规范独立克隆检查通过,核对后端 547 个、前端 105 个受跟踪文件。 +- [~] 新工具链提交后仍需在规范克隆执行完整发布构建;Linux 原生及双平台完整门禁未完成,P0-04 整项仍为进行中。 +- [!] 远端继续运行此前已验证的 0bb427d 正式候选,未将新的本地工具链或复审包冒充为已部署。其余生产门禁按下节继续。 + ## 最新进度(2026-09-30 20:53,回滚实机证据与 P0-03 独立复审) 本节及对应任务正文是当前状态;下方按日期保留的“总体验收仍为 P0-03”属于历史口径。任务复审和整体生产放行分开判断,不能将所有后续门禁都挂在目录整理任务下。