openone-workflow-kit 0.1.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.
Files changed (109) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/CODE_OF_CONDUCT.md +25 -0
  3. package/CONTRIBUTING.md +51 -0
  4. package/INIT.md +81 -0
  5. package/LICENSE +201 -0
  6. package/NOTICE +6 -0
  7. package/README.md +195 -0
  8. package/SECURITY.md +39 -0
  9. package/bin/check-contract.cjs +137 -0
  10. package/bin/check-sanitized.cjs +93 -0
  11. package/bin/init-workspace.cjs +966 -0
  12. package/docs/assets/PROVENANCE.md +20 -0
  13. package/docs/assets/architecture.svg +112 -0
  14. package/docs/assets/hero.svg +83 -0
  15. package/docs/assets/quick-demo.svg +74 -0
  16. package/docs/assets/social-preview.svg +57 -0
  17. package/docs/assets/visual-manifest.json +52 -0
  18. package/docs/definition-of-done.md +80 -0
  19. package/docs/dual-track-workflow.md +90 -0
  20. package/docs/local-merge-notes.md +21 -0
  21. package/docs/maintainer-handoff.md +150 -0
  22. package/docs/manual-publish.md +119 -0
  23. package/docs/private-denylist.example.txt +11 -0
  24. package/docs/publication-decisions.md +36 -0
  25. package/docs/release-checklist.md +72 -0
  26. package/docs/releases/v0.1.0.md +70 -0
  27. package/docs/shareable-install.md +51 -0
  28. package/docs/tool-install-recipes.md +103 -0
  29. package/examples/ecommerce/team-profile.example.yaml +141 -0
  30. package/examples/education/team-profile.example.yaml +143 -0
  31. package/examples/saas-to-b/team-profile.example.yaml +130 -0
  32. package/examples/team-profile.example.yaml +69 -0
  33. package/install.sh +27 -0
  34. package/package.json +65 -0
  35. package/scripts/build-release.cjs +141 -0
  36. package/scripts/release-source-state.cjs +38 -0
  37. package/templates/AGENTS.template.md +26 -0
  38. package/test/release-readiness.cjs +214 -0
  39. package/test/smoke.cjs +272 -0
  40. package/workflow/adapters/README.md +16 -0
  41. package/workflow/core/README.md +38 -0
  42. package/workflow/core/capabilities/README.md +61 -0
  43. package/workflow/core/capabilities/acceptance-oracle-tracker.md +45 -0
  44. package/workflow/core/capabilities/branch-gatekeeper.md +57 -0
  45. package/workflow/core/capabilities/channel-experiment-tracker.md +32 -0
  46. package/workflow/core/capabilities/ci-cd-automation-governor.md +123 -0
  47. package/workflow/core/capabilities/contract-tracer.md +53 -0
  48. package/workflow/core/capabilities/data-change-safety-checker.md +52 -0
  49. package/workflow/core/capabilities/definition-lint.md +81 -0
  50. package/workflow/core/capabilities/deployment-readiness-checker.md +57 -0
  51. package/workflow/core/capabilities/impact-scope-analyzer.md +50 -0
  52. package/workflow/core/capabilities/knowledge-capture-maintainer.md +32 -0
  53. package/workflow/core/capabilities/market-evidence-grader.md +33 -0
  54. package/workflow/core/capabilities/memory-curator.md +48 -0
  55. package/workflow/core/capabilities/personal-git-operator.md +41 -0
  56. package/workflow/core/capabilities/personal-release-checklist.md +49 -0
  57. package/workflow/core/capabilities/prd-code-diff-checker.md +53 -0
  58. package/workflow/core/capabilities/protocol-state-machine-checker.md +57 -0
  59. package/workflow/core/capabilities/release-safety-checker.md +61 -0
  60. package/workflow/core/capabilities/repo-baseline-scanner.md +48 -0
  61. package/workflow/core/capabilities/rule-extractor.md +48 -0
  62. package/workflow/core/capabilities/runtime-evidence-triage.md +53 -0
  63. package/workflow/core/capabilities/security-reviewer.md +48 -0
  64. package/workflow/core/capabilities/test-evidence-reviewer.md +50 -0
  65. package/workflow/core/capabilities/ui-baseline-reviewer.md +50 -0
  66. package/workflow/core/capabilities/verify-app.md +51 -0
  67. package/workflow/core/capabilities/worktree-isolator.md +53 -0
  68. package/workflow/core/commands/01-/351/234/200/346/261/202/350/256/250/350/256/272.md +36 -0
  69. package/workflow/core/commands/02-/344/272/247/345/223/201/346/226/207/346/241/243.md +35 -0
  70. package/workflow/core/commands/02B-UI/350/256/276/350/256/241.md +75 -0
  71. package/workflow/core/commands/03-06-/347/240/224/345/217/221/345/207/206/345/244/207.md +30 -0
  72. package/workflow/core/commands/03-/346/212/200/346/234/257/346/236/266/346/236/204.md +32 -0
  73. package/workflow/core/commands/04-/344/273/243/347/240/201/345/256/236/347/216/260.md +32 -0
  74. package/workflow/core/commands/04A-/345/211/215/347/253/257/344/273/243/347/240/201/345/256/236/347/216/260.md +36 -0
  75. package/workflow/core/commands/04B-/345/220/216/347/253/257/344/273/243/347/240/201/345/256/236/347/216/260.md +31 -0
  76. package/workflow/core/commands/05-/344/273/243/347/240/201/345/256/241/346/237/245.md +31 -0
  77. package/workflow/core/commands/06-/346/265/213/350/257/225/347/224/250/344/276/213.md +34 -0
  78. package/workflow/core/commands/07-/346/265/213/350/257/225/346/211/247/350/241/214.md +33 -0
  79. package/workflow/core/commands/08-/345/217/221/345/270/203/345/207/206/345/244/207.md +44 -0
  80. package/workflow/core/commands/09-/345/217/221/345/270/203/346/211/247/350/241/214.md +39 -0
  81. package/workflow/core/commands/10-/345/244/215/347/233/230/346/200/273/347/273/223.md +41 -0
  82. package/workflow/core/commands/B1-B8-/345/225/206/344/270/232/345/214/226/345/207/206/345/244/207.md +30 -0
  83. package/workflow/core/commands/B1-/344/270/232/345/212/241/345/256/232/344/275/215.md +38 -0
  84. package/workflow/core/commands/B2-/345/225/206/344/270/232/346/250/241/345/274/217.md +38 -0
  85. package/workflow/core/commands/B3-PMF/344/270/216/345/256/242/346/210/267/347/224/273/345/203/217.md +39 -0
  86. package/workflow/core/commands/B4-/345/234/272/346/231/257/344/270/216/350/264/255/344/271/260/346/227/205/347/250/213.md +37 -0
  87. package/workflow/core/commands/B5-/346/270/240/351/201/223/346/274/217/346/226/227/346/230/240/345/260/204.md +39 -0
  88. package/workflow/core/commands/B6-/350/220/245/351/224/200/350/216/267/345/256/242/347/255/226/347/225/245.md +38 -0
  89. package/workflow/core/commands/B7-/350/220/245/351/224/200/351/242/204/347/256/227.md +36 -0
  90. package/workflow/core/commands/B8-/346/270/240/351/201/223/346/211/247/350/241/214/347/255/226/347/225/245.md +39 -0
  91. package/workflow/core/commands/B9-/347/255/226/347/225/245/345/244/215/347/233/230.md +38 -0
  92. package/workflow/core/commands/README.md +56 -0
  93. package/workflow/core/commands/init-workspace.md +29 -0
  94. package/workflow/core/commands/new-feature.md +33 -0
  95. package/workflow/core/commands/new-product.md +28 -0
  96. package/workflow/core/commands/workflow-status.md +30 -0
  97. package/workflow/core/commands//344/270/200/350/207/264/346/200/247/346/243/200/346/237/245.md +35 -0
  98. package/workflow/core/commands//344/272/244/344/273/230/350/207/263/345/256/214/346/210/220.md +40 -0
  99. package/workflow/core/commands//345/256/232/344/271/211/345/256/214/346/210/220.md +41 -0
  100. package/workflow/core/commands//346/276/204/346/270/205.md +31 -0
  101. package/workflow/core/templates/00-business-status.md +43 -0
  102. package/workflow/core/templates/00-workflow-status.md +50 -0
  103. package/workflow/core/templates/README.md +12 -0
  104. package/workflow/core/templates/business-stage-document.md +49 -0
  105. package/workflow/core/templates/completion-contract.md +114 -0
  106. package/workflow/core/templates/constitution.template.md +50 -0
  107. package/workflow/core/templates/living-spec.md +35 -0
  108. package/workflow/core/templates/stage-document.md +42 -0
  109. package/workflow/core/templates/team-profile.template.yaml +104 -0
@@ -0,0 +1,141 @@
1
+ #!/usr/bin/env node
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const crypto = require('crypto');
5
+ const { spawnSync } = require('child_process');
6
+ const {
7
+ assertCleanReleaseSource,
8
+ readReleaseSourceState,
9
+ releaseSourceManifestLines
10
+ } = require('./release-source-state.cjs');
11
+
12
+ const root = path.resolve(__dirname, '..');
13
+ const dist = path.join(root, 'dist');
14
+ const localNpmCache = path.join(dist, '.npm-cache');
15
+ const localMiseCache = path.join(dist, '.mise-cache');
16
+ const sourceState = readReleaseSourceState(root);
17
+
18
+ assertCleanReleaseSource(sourceState);
19
+
20
+ fs.rmSync(dist, { recursive: true, force: true });
21
+ fs.mkdirSync(dist, { recursive: true });
22
+ fs.mkdirSync(localNpmCache, { recursive: true });
23
+ fs.mkdirSync(localMiseCache, { recursive: true });
24
+
25
+ const packageJson = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
26
+
27
+ runNpm(['run', 'check', '--', '--skip-release-build']);
28
+
29
+ const pack = runNpm(['pack', '--pack-destination', dist, '--json']);
30
+ let packed;
31
+ try {
32
+ packed = JSON.parse(pack.stdout)[0];
33
+ } catch (error) {
34
+ throw new Error(`无法解析 npm pack 输出: ${pack.stdout}`);
35
+ }
36
+
37
+ const tarball = path.join(dist, packed.filename);
38
+ const installSmokeRoot = path.join(dist, 'install-smoke');
39
+ const installPrefix = path.join(installSmokeRoot, 'consumer');
40
+ const installTarget = path.join(installSmokeRoot, 'target-workspace');
41
+
42
+ fs.rmSync(installSmokeRoot, { recursive: true, force: true });
43
+ fs.mkdirSync(installPrefix, { recursive: true });
44
+ fs.mkdirSync(path.join(installTarget, 'docs'), { recursive: true });
45
+ fs.mkdirSync(path.join(installTarget, 'app'), { recursive: true });
46
+ fs.writeFileSync(path.join(installTarget, 'docs', 'business-overview.md'), '# Business overview\n');
47
+ fs.writeFileSync(path.join(installTarget, 'app', 'package.json'), '{"dependencies":{"react":"latest"}}\n');
48
+
49
+ runNpm(['install', '--prefix', installPrefix, '--no-audit', '--no-fund', tarball]);
50
+ const installedPackageRoot = path.join(installPrefix, 'node_modules', packageJson.name);
51
+ // The npm package intentionally excludes repository-only GitHub configuration.
52
+ // Run the public package's own checks after installation to prove that all
53
+ // packaged release assets remain self-consistent without relying on `.github/`.
54
+ runNpm(['--prefix', installedPackageRoot, 'run', 'check']);
55
+ // Invoke the installed CommonJS entry with Node instead of executing npm's
56
+ // platform-specific .bin shim. This keeps the release smoke identical on
57
+ // Linux, macOS and Windows runners.
58
+ const installedEntry = path.join(
59
+ installedPackageRoot,
60
+ 'bin',
61
+ 'init-workspace.cjs'
62
+ );
63
+ run(process.execPath, [installedEntry, '--target', installTarget, '--tools', 'codex,trea,codebuddy', '--yes']);
64
+ for (const rel of [
65
+ 'AGENTS.md',
66
+ '.trae/instructions.md',
67
+ '.codebuddy/instructions.md',
68
+ 'workflow/team-profile.yaml',
69
+ 'workflow/INITIALIZATION_QUESTIONS.md'
70
+ ]) {
71
+ const file = path.join(installTarget, rel);
72
+ if (!fs.existsSync(file)) throw new Error(`发布包安装 smoke 缺少 ${rel}`);
73
+ }
74
+
75
+ const bytes = fs.readFileSync(tarball);
76
+ const sha256 = crypto.createHash('sha256').update(bytes).digest('hex');
77
+ const finalSourceState = readReleaseSourceState(root);
78
+ assertCleanReleaseSource(finalSourceState);
79
+ if (finalSourceState.commit !== sourceState.commit || finalSourceState.tree !== sourceState.tree) {
80
+ throw new Error('发布构建期间 source commit 或 source tree 发生变化;拒绝生成无法绑定 reviewed commit 的 manifest');
81
+ }
82
+ const manifest = [
83
+ '# 发布清单',
84
+ '',
85
+ `- package: ${packageJson.name}`,
86
+ `- version: ${packageJson.version}`,
87
+ `- license: ${packageJson.license}`,
88
+ `- tarball: ${packed.filename}`,
89
+ `- size: ${packed.size}`,
90
+ `- unpacked_size: ${packed.unpackedSize}`,
91
+ `- sha256: ${sha256}`,
92
+ ...releaseSourceManifestLines(sourceState),
93
+ '- install_smoke: passed',
94
+ '- installed_package_check: passed',
95
+ `- generated_at: ${new Date().toISOString()}`,
96
+ '',
97
+ '## 文件内容',
98
+ '',
99
+ ...packed.files.map((file) => `- ${file.path} (${file.size} bytes)`),
100
+ '',
101
+ '## 人工发布边界',
102
+ '',
103
+ '本清单由本地构建生成。创建远程仓库、git push、创建 tag、npm publish 或其他远程写入动作都必须由维护者手动执行。',
104
+ ''
105
+ ].join('\n');
106
+
107
+ fs.rmSync(installSmokeRoot, { recursive: true, force: true });
108
+ fs.rmSync(localNpmCache, { recursive: true, force: true });
109
+ fs.rmSync(localMiseCache, { recursive: true, force: true });
110
+
111
+ fs.writeFileSync(path.join(dist, 'RELEASE_MANIFEST.md'), manifest);
112
+ console.log(manifest);
113
+
114
+ function runNpm(args) {
115
+ // npm exposes the exact CLI entry while running an npm script. Invoking that
116
+ // entry with Node avoids .cmd resolution differences on Windows.
117
+ const npmExecPath = process.env.npm_execpath;
118
+ if (npmExecPath && fs.existsSync(npmExecPath)) {
119
+ return run(process.execPath, [npmExecPath, ...args]);
120
+ }
121
+ if (process.platform === 'win32') {
122
+ return run(process.env.ComSpec || 'cmd.exe', ['/d', '/s', '/c', 'npm', ...args]);
123
+ }
124
+ return run('npm', args);
125
+ }
126
+
127
+ function run(cmd, args) {
128
+ const result = spawnSync(cmd, args, {
129
+ cwd: root,
130
+ encoding: 'utf8',
131
+ env: Object.assign({}, process.env, {
132
+ AGENT_WORKFLOW_SKIP_RELEASE_BUILD: '1',
133
+ npm_config_cache: localNpmCache,
134
+ MISE_CACHE_DIR: localMiseCache
135
+ })
136
+ });
137
+ if (result.status !== 0) {
138
+ throw new Error(`${cmd} ${args.join(' ')} 执行失败\nSTDOUT:\n${result.stdout}\nSTDERR:\n${result.stderr}`);
139
+ }
140
+ return result;
141
+ }
@@ -0,0 +1,38 @@
1
+ const { spawnSync } = require('child_process');
2
+
3
+ function readReleaseSourceState(root) {
4
+ const commit = gitOutput(root, ['rev-parse', 'HEAD']);
5
+ const tree = gitOutput(root, ['rev-parse', 'HEAD^{tree}']);
6
+ const status = gitOutput(root, ['status', '--porcelain=v1', '--untracked-files=all']);
7
+ return Object.freeze({ commit, tree, status, dirty: status.length > 0 });
8
+ }
9
+
10
+ function assertCleanReleaseSource(state) {
11
+ if (!state || state.dirty || state.status) {
12
+ const detail = state && state.status ? `\n${state.status}` : '';
13
+ throw new Error(`发布构建只允许从 clean reviewed commit 生成;请先审查并提交本地变更,再重跑 npm run build:release${detail}`);
14
+ }
15
+ }
16
+
17
+ function releaseSourceManifestLines(state) {
18
+ assertCleanReleaseSource(state);
19
+ return [
20
+ `- source_commit: ${state.commit}`,
21
+ `- source_tree: ${state.tree}`,
22
+ '- source_dirty: false'
23
+ ];
24
+ }
25
+
26
+ function gitOutput(root, args) {
27
+ const result = spawnSync('git', args, { cwd: root, encoding: 'utf8' });
28
+ if (result.status !== 0) {
29
+ throw new Error(`git ${args.join(' ')} 执行失败: ${result.stderr || result.stdout}`);
30
+ }
31
+ return result.stdout.trim();
32
+ }
33
+
34
+ module.exports = {
35
+ assertCleanReleaseSource,
36
+ readReleaseSourceState,
37
+ releaseSourceManifestLines
38
+ };
@@ -0,0 +1,26 @@
1
+ # Agent Workflow
2
+
3
+ 本工作区使用 OpenOne Workflow Kit 生成的个人开发者工作流。
4
+
5
+ ## 事实源
6
+
7
+ - 项目配置:`workflow/team-profile.yaml`
8
+ - Core 工作流:`workflow/core/`
9
+ - 可复用能力:`workflow/core/capabilities/`
10
+ - 工具 adapter:只作为生成的薄入口
11
+
12
+ ## 硬闸门
13
+
14
+ - 功能分支闸门和实现阶段闸门通过前,不得修改业务代码。
15
+ - 个人项目允许 agent 在范围明确且工作树已检查后执行本地分支命名、创建、commit、tag 和本地 merge。
16
+ - 远程 push、release、部署、数据库写入和生产配置写入需要用户明确授权。
17
+ - 进入实现阶段后,同仓多需求并行必须使用独立 worktree。
18
+ - 缺失本地资料时,必须向用户索要或记录到 `workflow/INITIALIZATION_QUESTIONS.md`。
19
+
20
+ ## 工具策略
21
+
22
+ 只能使用当前工具自己的 adapter。不要承诺所有工具体验完全一致。
23
+
24
+ ## 开始
25
+
26
+ 先读取 `workflow/team-profile.yaml`,再按 `workflow/core/commands/` 执行对应阶段。
@@ -0,0 +1,214 @@
1
+ #!/usr/bin/env node
2
+ const crypto = require('crypto');
3
+ const fs = require('fs');
4
+ const os = require('os');
5
+ const path = require('path');
6
+ const { spawnSync } = require('child_process');
7
+ const {
8
+ assertCleanReleaseSource,
9
+ readReleaseSourceState,
10
+ releaseSourceManifestLines
11
+ } = require('../scripts/release-source-state.cjs');
12
+
13
+ const root = path.resolve(__dirname, '..');
14
+ const pkg = readJson('package.json');
15
+ const failures = [];
16
+
17
+ expect(pkg.name === 'openone-workflow-kit', 'package name must be openone-workflow-kit');
18
+ expect(pkg.version === '0.1.0', 'package version must be 0.1.0');
19
+ expect(pkg.private === false, 'package must be publishable');
20
+ expect(pkg.license === 'Apache-2.0', 'package license must be Apache-2.0');
21
+ expect(pkg.engines && pkg.engines.node === '>=18', 'Node.js support must start at 18');
22
+ expect(pkg.publishConfig && pkg.publishConfig.access === 'public', 'npm access must be public');
23
+ expect(
24
+ pkg.repository && pkg.repository.url === 'git+https://github.com/bluecoast1379/openone-workflow-kit.git',
25
+ 'repository URL must point to the public GitHub repository'
26
+ );
27
+ expect(
28
+ pkg.homepage === 'https://github.com/bluecoast1379/openone-workflow-kit#readme',
29
+ 'homepage must point to the repository README'
30
+ );
31
+ expect(
32
+ pkg.bugs && pkg.bugs.url === 'https://github.com/bluecoast1379/openone-workflow-kit/issues',
33
+ 'bugs URL must point to GitHub Issues'
34
+ );
35
+
36
+ for (const required of [
37
+ 'README.md',
38
+ 'CHANGELOG.md',
39
+ 'LICENSE',
40
+ 'NOTICE',
41
+ 'bin/',
42
+ 'scripts/',
43
+ 'workflow/',
44
+ 'templates/',
45
+ 'examples/',
46
+ 'docs/',
47
+ 'test/'
48
+ ]) {
49
+ expect(pkg.files.includes(required), `package files must include ${required}`);
50
+ }
51
+
52
+ for (const [name, relative] of Object.entries(pkg.bin || {})) {
53
+ const file = path.join(root, relative);
54
+ expect(fs.existsSync(file), `bin ${name} must exist at ${relative}`);
55
+ if (fs.existsSync(file)) {
56
+ expect(fs.readFileSync(file, 'utf8').startsWith('#!/usr/bin/env node'), `bin ${name} must have a Node.js shebang`);
57
+ }
58
+ }
59
+
60
+ const readme = fs.readFileSync(path.join(root, 'README.md'), 'utf8');
61
+ for (const marker of [
62
+ './docs/assets/hero.svg',
63
+ './docs/assets/quick-demo.svg',
64
+ './docs/assets/architecture.svg',
65
+ '## 30 秒 Quick Demo',
66
+ 'openone-workflow-kit@0.1.0',
67
+ 'open-workflow-kit',
68
+ 'business-agent'
69
+ ]) {
70
+ expect(readme.includes(marker), `README must include ${marker}`);
71
+ }
72
+
73
+ const manifest = readJson('docs/assets/visual-manifest.json');
74
+ for (const asset of manifest.assets || []) {
75
+ const file = path.join(root, 'docs', 'assets', asset.file);
76
+ expect(fs.existsSync(file), `visual asset must exist: ${asset.file}`);
77
+ if (!fs.existsSync(file)) continue;
78
+ const content = fs.readFileSync(file);
79
+ const text = content.toString('utf8');
80
+ const svgStart = text.match(/<svg\b[^>]*>/);
81
+ expect(Boolean(svgStart), `${asset.file} must contain an SVG root`);
82
+ if (svgStart) {
83
+ expect(new RegExp(`width=["']${asset.width}["']`).test(svgStart[0]), `${asset.file} width must be ${asset.width}`);
84
+ expect(new RegExp(`height=["']${asset.height}["']`).test(svgStart[0]), `${asset.file} height must be ${asset.height}`);
85
+ expect(
86
+ new RegExp(`viewBox=["']0 0 ${asset.width} ${asset.height}["']`).test(svgStart[0]),
87
+ `${asset.file} viewBox must match its dimensions`
88
+ );
89
+ }
90
+ expect(content.length <= 250 * 1024, `${asset.file} must be no larger than 250 KB`);
91
+ expect(text.includes('<title'), `${asset.file} must include an accessible title`);
92
+ expect(text.includes('<desc'), `${asset.file} must include an accessible description`);
93
+ expect(
94
+ crypto.createHash('sha256').update(content).digest('hex') === asset.sha256,
95
+ `${asset.file} checksum must match visual-manifest.json`
96
+ );
97
+ }
98
+
99
+ const sourceCheckout = fs.existsSync(path.join(root, '.git'));
100
+ if (sourceCheckout) {
101
+ const attributesFile = path.join(root, '.gitattributes');
102
+ expect(fs.existsSync(attributesFile), 'source checkout must pin release-evidence line endings');
103
+ if (fs.existsSync(attributesFile)) {
104
+ const attributes = fs.readFileSync(attributesFile, 'utf8');
105
+ expect(
106
+ /^\*\.svg\s+text\s+eol=lf$/m.test(attributes),
107
+ 'source checkout must keep SVG checksum evidence on LF line endings'
108
+ );
109
+ }
110
+ const workflowFile = path.join(root, '.github', 'workflows', 'check.yml');
111
+ expect(fs.existsSync(workflowFile), 'source checkout must include .github/workflows/check.yml');
112
+ if (fs.existsSync(workflowFile)) {
113
+ const workflow = fs.readFileSync(workflowFile, 'utf8');
114
+ expect(/permissions:\s*\n\s+contents: read/.test(workflow), 'CI permissions must be read-only');
115
+ for (const action of workflow.matchAll(/uses:\s*([^\s]+)/g)) {
116
+ expect(/@[0-9a-f]{40}$/.test(action[1]), `CI action must be pinned by commit SHA: ${action[1]}`);
117
+ }
118
+ for (const expected of [
119
+ 'Ubuntu / Node 18',
120
+ 'Ubuntu / Node 20',
121
+ 'Ubuntu / Node 22',
122
+ 'Windows / Node 20',
123
+ 'macOS / Node 20'
124
+ ]) {
125
+ expect(workflow.includes(expected), `CI matrix must include ${expected}`);
126
+ }
127
+ }
128
+ }
129
+
130
+ expect(fs.existsSync(path.join(root, 'docs', 'releases', 'v0.1.0.md')), 'v0.1.0 release notes must exist');
131
+ const manualPublish = fs.readFileSync(path.join(root, 'docs', 'manual-publish.md'), 'utf8');
132
+ for (const marker of [
133
+ 'npm publish --access public',
134
+ 'npm view openone-workflow-kit@0.1.0 gitHead',
135
+ 'npm view openone-workflow-kit@0.1.0 dist.shasum',
136
+ 'dist/openone-workflow-kit-0.1.0.tgz',
137
+ 'dist/RELEASE_MANIFEST.md'
138
+ ]) {
139
+ expect(manualPublish.includes(marker), `manual publish guide must include ${marker}`);
140
+ }
141
+ expect(!manualPublish.includes('npm publish ./dist/'), 'npm publication must run from the clean reviewed commit directory');
142
+ verifyReleaseSourceBinding();
143
+
144
+ if (failures.length) {
145
+ for (const failure of failures) console.error(`- ${failure}`);
146
+ process.exit(1);
147
+ }
148
+
149
+ console.log('Release readiness checks passed.');
150
+
151
+ function readJson(relative) {
152
+ return JSON.parse(fs.readFileSync(path.join(root, relative), 'utf8'));
153
+ }
154
+
155
+ function expect(condition, message) {
156
+ if (!condition) failures.push(message);
157
+ }
158
+
159
+ function verifyReleaseSourceBinding() {
160
+ const fixture = fs.mkdtempSync(path.join(os.tmpdir(), 'openone-release-source-'));
161
+ try {
162
+ runGit(fixture, ['init']);
163
+ runGit(fixture, ['config', 'user.name', 'OpenOne Release Test']);
164
+ runGit(fixture, ['config', 'user.email', 'release-test@example.invalid']);
165
+ fs.writeFileSync(path.join(fixture, 'fixture.txt'), 'reviewed\n');
166
+ runGit(fixture, ['add', 'fixture.txt']);
167
+ runGit(fixture, ['-c', 'commit.gpgsign=false', 'commit', '-m', 'Create reviewed fixture']);
168
+
169
+ const clean = readReleaseSourceState(fixture);
170
+ expect(clean.dirty === false && clean.status === '', 'release source preflight must accept a clean commit');
171
+ expect(/^[0-9a-f]{40,64}$/.test(clean.commit), 'release source preflight must capture the source commit');
172
+ expect(/^[0-9a-f]{40,64}$/.test(clean.tree), 'release source preflight must capture the source tree');
173
+ expect(
174
+ JSON.stringify(releaseSourceManifestLines(clean)) === JSON.stringify([
175
+ `- source_commit: ${clean.commit}`,
176
+ `- source_tree: ${clean.tree}`,
177
+ '- source_dirty: false'
178
+ ]),
179
+ 'release manifest must bind the clean source commit and tree'
180
+ );
181
+
182
+ fs.writeFileSync(path.join(fixture, 'unreviewed.txt'), 'dirty\n');
183
+ const dirty = readReleaseSourceState(fixture);
184
+ let rejected = false;
185
+ try {
186
+ assertCleanReleaseSource(dirty);
187
+ } catch (error) {
188
+ rejected = /clean reviewed commit/.test(String(error && error.message));
189
+ }
190
+ expect(dirty.dirty === true && dirty.status.includes('unreviewed.txt'), 'release source preflight must detect untracked changes');
191
+ expect(rejected, 'release source preflight must reject a dirty worktree');
192
+
193
+ fs.rmSync(path.join(fixture, 'unreviewed.txt'));
194
+ fs.writeFileSync(path.join(fixture, 'fixture.txt'), 'modified after review\n');
195
+ const modified = readReleaseSourceState(fixture);
196
+ let modifiedRejected = false;
197
+ try {
198
+ assertCleanReleaseSource(modified);
199
+ } catch (error) {
200
+ modifiedRejected = /clean reviewed commit/.test(String(error && error.message));
201
+ }
202
+ expect(modified.dirty === true && modified.status.includes('fixture.txt'), 'release source preflight must detect tracked changes');
203
+ expect(modifiedRejected, 'release source preflight must reject tracked changes');
204
+ } finally {
205
+ fs.rmSync(fixture, { recursive: true, force: true });
206
+ }
207
+ }
208
+
209
+ function runGit(cwd, args) {
210
+ const result = spawnSync('git', args, { cwd, encoding: 'utf8' });
211
+ if (result.status !== 0) {
212
+ throw new Error(`git ${args.join(' ')} failed: ${result.stderr || result.stdout}`);
213
+ }
214
+ }