@accelerator-mzq/forge 3.0.0 → 4.0.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 (105) hide show
  1. package/README.md +12 -12
  2. package/dist/cli/commands/ack.d.ts.map +1 -1
  3. package/dist/cli/commands/ack.js +87 -12
  4. package/dist/cli/commands/ack.js.map +1 -1
  5. package/dist/cli/commands/archive.d.ts +32 -33
  6. package/dist/cli/commands/archive.d.ts.map +1 -1
  7. package/dist/cli/commands/archive.js +339 -667
  8. package/dist/cli/commands/archive.js.map +1 -1
  9. package/dist/cli/commands/legacy-bridge.js +1 -1
  10. package/dist/cli/commands/legacy-bridge.js.map +1 -1
  11. package/dist/cli/commands/upgrade.d.ts.map +1 -1
  12. package/dist/cli/commands/upgrade.js +2 -51
  13. package/dist/cli/commands/upgrade.js.map +1 -1
  14. package/dist/cli/commands/validate.d.ts.map +1 -1
  15. package/dist/cli/commands/validate.js +4 -25
  16. package/dist/cli/commands/validate.js.map +1 -1
  17. package/dist/cli/index.js +0 -9
  18. package/dist/cli/index.js.map +1 -1
  19. package/dist/core/ack/marker-ack.d.ts +28 -0
  20. package/dist/core/ack/marker-ack.d.ts.map +1 -0
  21. package/dist/core/ack/marker-ack.js +155 -0
  22. package/dist/core/ack/marker-ack.js.map +1 -0
  23. package/dist/core/ack-log.d.ts.map +1 -1
  24. package/dist/core/ack-log.js +10 -7
  25. package/dist/core/ack-log.js.map +1 -1
  26. package/dist/core/archive/index.d.ts +2 -3
  27. package/dist/core/archive/index.d.ts.map +1 -1
  28. package/dist/core/archive/index.js +7 -5
  29. package/dist/core/archive/index.js.map +1 -1
  30. package/dist/core/archive/pause-decisions-fence.js +12 -12
  31. package/dist/core/archive/pause-decisions-fence.js.map +1 -1
  32. package/dist/core/archive/summary-builder.d.ts +26 -21
  33. package/dist/core/archive/summary-builder.d.ts.map +1 -1
  34. package/dist/core/archive/summary-builder.js +115 -223
  35. package/dist/core/archive/summary-builder.js.map +1 -1
  36. package/dist/core/archive/summary-render.d.ts +5 -3
  37. package/dist/core/archive/summary-render.d.ts.map +1 -1
  38. package/dist/core/archive/summary-render.js +38 -44
  39. package/dist/core/archive/summary-render.js.map +1 -1
  40. package/dist/core/lock.d.ts +46 -0
  41. package/dist/core/lock.d.ts.map +1 -0
  42. package/dist/core/lock.js +98 -0
  43. package/dist/core/lock.js.map +1 -0
  44. package/dist/core/markers/types.d.ts +27 -134
  45. package/dist/core/markers/types.d.ts.map +1 -1
  46. package/dist/core/markers/types.js +10 -1
  47. package/dist/core/markers/types.js.map +1 -1
  48. package/dist/core/migrate/index.js +1 -1
  49. package/dist/core/migrate/index.js.map +1 -1
  50. package/dist/core/monitor/artifact-observer.d.ts.map +1 -1
  51. package/dist/core/monitor/artifact-observer.js +28 -78
  52. package/dist/core/monitor/artifact-observer.js.map +1 -1
  53. package/dist/core/monitor/divergence-map.d.ts.map +1 -1
  54. package/dist/core/monitor/divergence-map.js +9 -7
  55. package/dist/core/monitor/divergence-map.js.map +1 -1
  56. package/dist/core/monitor/health-verdict.d.ts.map +1 -1
  57. package/dist/core/monitor/health-verdict.js +2 -1
  58. package/dist/core/monitor/health-verdict.js.map +1 -1
  59. package/dist/core/monitor/trace-store.d.ts +1 -1
  60. package/dist/core/monitor/trace-store.js +2 -2
  61. package/dist/core/monitor/trace-store.js.map +1 -1
  62. package/dist/core/monitor/types.d.ts +1 -1
  63. package/dist/core/monitor/types.d.ts.map +1 -1
  64. package/dist/core/monitor/types.js +0 -1
  65. package/dist/core/monitor/types.js.map +1 -1
  66. package/dist/core/schemas/archive-summary.d.ts +39 -109
  67. package/dist/core/schemas/archive-summary.d.ts.map +1 -1
  68. package/dist/core/schemas/archive-summary.js +15 -23
  69. package/dist/core/schemas/archive-summary.js.map +1 -1
  70. package/dist/core/templates/commands/ack-confirm.md +1 -1
  71. package/dist/core/templates/commands/apply.md +51 -114
  72. package/dist/core/templates/commands/archive.md +43 -160
  73. package/dist/core/templates/commands/review.md +49 -74
  74. package/dist/core/templates/commands/upgrade.md +21 -40
  75. package/dist/core/templates/commands/verify.md +43 -146
  76. package/dist/core/templates/skills/_shared/tier23-command-bridge.md +34 -0
  77. package/dist/core/templates/skills/exploring.md +3 -3
  78. package/dist/core/templates/skills/finishing-a-development-branch.md +6 -21
  79. package/dist/core/templates/skills/receiving-code-review.md +14 -45
  80. package/dist/core/templates/skills/requesting-code-review.md +11 -0
  81. package/dist/core/templates/skills/subagent-driven-development.md +33 -29
  82. package/dist/core/templates/skills/subagent-driven-discipline.md +28 -28
  83. package/dist/core/templates/skills/using-forge.md +25 -24
  84. package/dist/core/templates/skills/verification-before-completion.md +7 -18
  85. package/dist/core/templates/skills/verifying-three-dimensions.md +93 -169
  86. package/dist/core/templates/skills/writing-plans.md +6 -17
  87. package/dist/core/validate/archive-summary-schema.d.ts +1 -1
  88. package/dist/core/validate/archive-summary-schema.d.ts.map +1 -1
  89. package/dist/core/validate/archive-summary-schema.js +101 -125
  90. package/dist/core/validate/archive-summary-schema.js.map +1 -1
  91. package/dist/core/validate/index.d.ts +0 -1
  92. package/dist/core/validate/index.d.ts.map +1 -1
  93. package/dist/core/validate/index.js +2 -1
  94. package/dist/core/validate/index.js.map +1 -1
  95. package/dist/core/validate/marker-schema.d.ts +12 -4
  96. package/dist/core/validate/marker-schema.d.ts.map +1 -1
  97. package/dist/core/validate/marker-schema.js +98 -605
  98. package/dist/core/validate/marker-schema.js.map +1 -1
  99. package/dist/index.d.ts +1 -1
  100. package/dist/index.d.ts.map +1 -1
  101. package/dist/index.js +8 -1
  102. package/dist/index.js.map +1 -1
  103. package/package.json +5 -4
  104. package/scripts/codex-review-helper.mjs +337 -0
  105. package/src/core/codex-review/prompts/adversarial-default.md +105 -0
@@ -1,676 +1,169 @@
1
- // Marker schema 类型校验 — spec §3.4.1
1
+ // Marker schema 类型校验 — v4 OpenSpec alignment 简版(plan-v4 Phase 1 Task 1.3)
2
+ //
3
+ // v4 BREAKING change:
4
+ // - 删 KNOWN_SCHEMAS 中 v1 schema + verify-failed/review-failed
5
+ // - 改 ISO_8601_RE 允许 ms 精度(V-1 修复)
6
+ // - 删 process_evidence / ack_log_* / verify_findings 复杂校验
7
+ // - 简化 PauseDecision schema 校验为 PauseDecisionSimple(5 字段)
2
8
  import { ok, failed, mergeResults } from './types.js';
3
- // plan-9d Task 3 (v12 REG-IMPORT-001 修订):import 9a 三级 severity enum + isSeverity
4
- // 用于 checkVerifyFindingsArray 校验 verify_findings[i].severity(CRITICAL/WARNING/SUGGESTION)
5
- // 现有 local const SEVERITY_VALUES rename REVIEW_OUTCOME_SEVERITY_CODES 避免重名冲突
6
- import { SEVERITY_VALUES, isSeverity } from '../schemas/severity.js';
7
- // 已知合法的 schema 名称
8
- const KNOWN_SCHEMAS = [
9
- 'forge-verify/v1',
10
- 'forge-review/v1',
11
- 'forge-verify-failed/v1',
12
- 'forge-review-failed/v1',
13
- ];
14
- // ISO 8601 UTC 格式正则(YYYY-MM-DDTHH:MM:SSZ)
15
- const ISO_8601_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/;
16
- // sha256: 前缀 + 64 位小写十六进制
17
- const SHA256_RE = /^sha256:[a-f0-9]{64}$/;
18
- // finding_hash:裸 64-hex(沿 9a finding-hash.ts:8,不带 sha256: 前缀)
19
- const FINDING_HASH_RE = /^[a-f0-9]{64}$/;
20
- // git commit hash:40 位小写十六进制
21
- const GIT_HEAD_RE = /^[a-f0-9]{40}$/;
22
- // plan-9j Task 1 新增:created_by_tool_version semver 校验
23
- // 沿 archive-summary.ts SEMVER_RE 同模式(major.minor.patch[-prerelease][+build])
9
+ // 已知合法的 schema 名称(v4 BREAKING:v2)
10
+ const KNOWN_SCHEMAS = ['forge-verify/v2', 'forge-review/v2'];
11
+ // ISO 8601 UTC 格式正则 v4 允许 ms 精度(沿 V-1 修复:YYYY-MM-DDTHH:MM:SS[.fff]Z)
12
+ const ISO_8601_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,9})?Z$/;
13
+ // semver(created_by_tool_version 用)
24
14
  const SEMVER_RE = /^\d+\.\d+\.\d+(?:-[\w.-]+)?(?:\+[\w.-]+)?$/;
25
- // actor 合法值
26
- const ACTOR_VALUES = new Set(['ai-agent', 'human-override']);
27
- // review_outcomes.severity 合法值(S/C/L 简码,语义独立于 9a 三级 enum CRITICAL/WARNING/SUGGESTION)
28
- // plan-9d Task 3 (v12 REG-IMPORT-001 修订):rename 自 SEVERITY_VALUES → REVIEW_OUTCOME_SEVERITY_CODES
29
- // v3 BLOCKER 4(plan-9j):union 扩 simcode + 全名(沿 types.ts ReviewOutcome.severity)
30
- const REVIEW_OUTCOME_SEVERITY_CODES = new Set([
31
- 'S',
32
- 'C',
33
- 'L', // v0.4 simcode(老 marker 兼容)
34
- 'CRITICAL',
35
- 'WARNING',
36
- 'SUGGESTION', // v1.0 全名(resign 后或 v1.0 native 写入)
37
- ]);
38
- // verify_findings.dimension 合法值(沿 design §2.2.2 三维度 + plan-9g brainstorm v10 加 process_evidence)
39
- const DIMENSION_VALUES = new Set([
40
- 'completeness',
41
- 'correctness',
42
- 'coherence',
43
- 'process_evidence', // plan-9g freeze-time WARNING 7/10(brainstorm spec §3 修订)
44
- ]);
45
- // plan-9c Task 1 新增:pause_decisions.chosen_option 合法值(1 | 2 | 3 | 4)
46
- const CHOSEN_OPTION_VALUES = new Set([1, 2, 3, 4]);
47
15
  /**
48
- * 校验 marker 对象的 schema 合法性。
49
- * @param m - 已解析的 marker 对象(AnyMarker 或 unknown)
50
- * @param file - 可选:来源文件路径,用于错误报告
16
+ * validateMarkerSchema — 校验 marker 文件的 schema 类型 + 必填字段
17
+ *
18
+ * v4 简版:仅校验
19
+ * - schema 字段是否在 KNOWN_SCHEMAS 中
20
+ * - verified_at / reviewed_at ISO 8601 (允许 ms)
21
+ * - verified_by / reviewed_by 字符串非空
22
+ * - pause_decisions 数组类型(若存在)+ 内字段类型
23
+ * - created_by_tool_version semver 格式(若存在)
24
+ *
25
+ * 不再校验:tasks_hash / content_hash / git / process_evidence / ack_log_* / verify_findings
26
+ * (v4 反加固协议砍后这些字段已从 marker 删除)
51
27
  */
52
- export function validateMarkerSchema(m, file) {
53
- if (!m || typeof m !== 'object') {
54
- return failed({ artifact: 'marker', message: 'marker must be a YAML mapping', file });
55
- }
56
- const obj = m;
57
- // 1. schema 字段必须是已知值之一
58
- if (typeof obj.schema !== 'string' || !KNOWN_SCHEMAS.includes(obj.schema)) {
28
+ export function validateMarkerSchema(obj, file) {
29
+ if (!obj || typeof obj !== 'object' || Array.isArray(obj)) {
59
30
  return failed({
60
31
  artifact: 'marker',
61
- field: 'schema',
62
- message: `schema must be one of: ${KNOWN_SCHEMAS.join(', ')}`,
32
+ message: 'marker 文件根必须是 object',
63
33
  file,
64
34
  });
65
35
  }
66
- const schema = obj.schema;
67
- const results = [];
68
- // 2. schema 类型分支校验各字段
69
- if (schema === 'forge-verify/v1') {
70
- results.push(checkTimestamp(obj, 'verified_at', file));
71
- results.push(checkActor(obj, 'verified_by', file));
72
- results.push(checkSha256(obj, 'tasks_hash', file));
73
- results.push(checkSha256(obj, 'content_hash', file));
74
- results.push(checkEvidenceArray(obj.evidence, file));
75
- // plan-9d Task 3 新增:verify_findings 数组校验(可选 superset additive,老 marker 缺等价 [])
76
- if (obj.verify_findings !== undefined) {
77
- results.push(checkVerifyFindingsArray(obj.verify_findings, file));
78
- }
79
- // plan-9c Task 1 新增 — pause_decisions superset additive
80
- if (obj.pause_decisions !== undefined) {
81
- results.push(checkPauseDecisionsArray(obj.pause_decisions, file));
82
- }
83
- // plan-9j Task 1 新增:created_by_tool_version 可选 superset additive 字段
84
- results.push(checkSemverString(obj, 'created_by_tool_version', file));
85
- // v3 BLOCKER 2 新增:resigned_by_tool_version(沿 v2 选项 C — 区分 created vs resigned)
86
- results.push(checkSemverString(obj, 'resigned_by_tool_version', file));
87
- }
88
- else if (schema === 'forge-review/v1') {
89
- results.push(checkTimestamp(obj, 'reviewed_at', file));
90
- results.push(checkActor(obj, 'reviewed_by', file));
91
- results.push(checkSha256(obj, 'tasks_hash', file));
92
- results.push(checkSha256(obj, 'content_hash', file));
93
- results.push(checkGitInfo(obj.git, file));
94
- results.push(checkReviewOutcomes(obj.review_outcomes, file));
95
- // plan-9c Task 1 新增
96
- if (obj.pause_decisions !== undefined) {
97
- results.push(checkPauseDecisionsArray(obj.pause_decisions, file));
98
- }
99
- // plan-9j Task 1 新增:created_by_tool_version 可选 superset additive 字段
100
- results.push(checkSemverString(obj, 'created_by_tool_version', file));
101
- // v3 BLOCKER 2 新增:resigned_by_tool_version(沿 v2 选项 C — 区分 created vs resigned)
102
- results.push(checkSemverString(obj, 'resigned_by_tool_version', file));
103
- }
104
- else if (schema === 'forge-verify-failed/v1') {
105
- results.push(checkTimestamp(obj, 'failed_at', file));
106
- results.push(checkStringArray(obj, 'reasons', file));
107
- results.push(checkStringArray(obj, 'fake_completions', file));
108
- results.push(checkStringArray(obj, 'appended_tasks', file));
109
- // plan-9d Task 3 新增:verify_findings 数组校验(可选 superset)
110
- if (obj.verify_findings !== undefined) {
111
- results.push(checkVerifyFindingsArray(obj.verify_findings, file));
112
- }
113
- // plan-9c Task 1 新增
114
- if (obj.pause_decisions !== undefined) {
115
- results.push(checkPauseDecisionsArray(obj.pause_decisions, file));
116
- }
117
- // plan-9j Task 1 新增:created_by_tool_version 可选 superset additive 字段
118
- results.push(checkSemverString(obj, 'created_by_tool_version', file));
119
- // v3 BLOCKER 2 新增:resigned_by_tool_version(沿 v2 选项 C — 区分 created vs resigned)
120
- results.push(checkSemverString(obj, 'resigned_by_tool_version', file));
121
- }
122
- else if (schema === 'forge-review-failed/v1') {
123
- results.push(checkTimestamp(obj, 'failed_at', file));
124
- if (!Array.isArray(obj.unresolved_outcomes)) {
125
- results.push(failed({
126
- artifact: 'marker',
127
- field: 'unresolved_outcomes',
128
- message: 'must be array',
129
- file,
130
- }));
131
- }
132
- results.push(checkStringArray(obj, 'appended_tasks', file));
133
- // plan-9c Task 1 新增
134
- if (obj.pause_decisions !== undefined) {
135
- results.push(checkPauseDecisionsArray(obj.pause_decisions, file));
136
- }
137
- // plan-9j Task 1 新增:created_by_tool_version 可选 superset additive 字段
138
- results.push(checkSemverString(obj, 'created_by_tool_version', file));
139
- // v3 BLOCKER 2 新增:resigned_by_tool_version(沿 v2 选项 C — 区分 created vs resigned)
140
- results.push(checkSemverString(obj, 'resigned_by_tool_version', file));
36
+ const m = obj;
37
+ // 1. schema 字段必填且合法
38
+ if (typeof m.schema !== 'string') {
39
+ return failed({ artifact: 'marker', field: 'schema', message: 'schema 必须是字符串', file });
141
40
  }
142
- return mergeResults(...results);
143
- }
144
- // 校验 ISO 8601 UTC 时间戳字段
145
- function checkTimestamp(obj, field, file) {
146
- const v = obj[field];
147
- if (typeof v !== 'string' || !ISO_8601_RE.test(v)) {
148
- return failed({
149
- artifact: 'marker',
150
- field,
151
- message: 'must be ISO 8601 UTC (YYYY-MM-DDTHH:MM:SSZ)',
152
- file,
153
- });
154
- }
155
- return ok();
156
- }
157
- // 校验 actor 字段(ai-agent 或 human-override)
158
- function checkActor(obj, field, file) {
159
- const v = obj[field];
160
- if (typeof v !== 'string' || !ACTOR_VALUES.has(v)) {
41
+ if (!KNOWN_SCHEMAS.includes(m.schema)) {
161
42
  return failed({
162
43
  artifact: 'marker',
163
- field,
164
- message: 'must be "ai-agent" or "human-override"',
44
+ field: 'schema',
45
+ message: `schema 必须 {${KNOWN_SCHEMAS.join(', ')}},实际:${m.schema}`,
165
46
  file,
166
47
  });
167
48
  }
168
- return ok();
169
- }
170
- // 校验 sha256: 前缀的哈希字段
171
- function checkSha256(obj, field, file) {
172
- const v = obj[field];
173
- if (typeof v !== 'string' || !SHA256_RE.test(v)) {
174
- return failed({ artifact: 'marker', field, message: 'must match ^sha256:[a-f0-9]{64}$', file });
175
- }
176
- return ok();
177
- }
178
- // plan-9j Task 1 新增:校验 semver 字符串字段(可选字段,缺失等价 <1.0.0)
179
- function checkSemverString(obj, field, file) {
180
- const v = obj[field];
181
- if (v === undefined) {
182
- return ok(); // 字段缺失等价老 marker(<1.0.0),superset additive 兼容
183
- }
184
- if (typeof v !== 'string' || !SEMVER_RE.test(v)) {
185
- return failed({
49
+ const results = [];
50
+ const kind = m.schema === 'forge-verify/v2' ? 'verify' : 'review';
51
+ // 2. timestamp 字段(verified_at / reviewed_at)
52
+ const atKey = kind === 'verify' ? 'verified_at' : 'reviewed_at';
53
+ results.push(checkTimestamp(m, atKey, file));
54
+ // 3. by 字段(verified_by / reviewed_by)— v4 不限定 enum,允许任意 string
55
+ const byKey = kind === 'verify' ? 'verified_by' : 'reviewed_by';
56
+ if (typeof m[byKey] !== 'string' || m[byKey].length === 0) {
57
+ results.push(failed({
186
58
  artifact: 'marker',
187
- field,
188
- message: `must be semver string (major.minor.patch[-prerelease][+build]) or omitted (老 marker <1.0.0,沿 design §3.4.1)`,
59
+ field: byKey,
60
+ message: `${byKey} 必须是非空字符串`,
189
61
  file,
190
- });
191
- }
192
- return ok();
193
- }
194
- // 校验字符串数组字段
195
- function checkStringArray(obj, field, file) {
196
- const v = obj[field];
197
- if (!Array.isArray(v) || !v.every((x) => typeof x === 'string')) {
198
- return failed({ artifact: 'marker', field, message: 'must be array of strings', file });
62
+ }));
199
63
  }
200
- return ok();
201
- }
202
- // 校验 evidence 数组(每项含 6 个必需字段)
203
- function checkEvidenceArray(v, file) {
204
- if (!Array.isArray(v)) {
205
- return failed({ artifact: 'marker', field: 'evidence', message: 'must be array', file });
64
+ // 4. pause_decisions(可选,数组类型 only)
65
+ if (m.pause_decisions !== undefined) {
66
+ results.push(checkPauseDecisionsArray(m.pause_decisions, file));
206
67
  }
207
- const results = [];
208
- for (let i = 0; i < v.length; i++) {
209
- const e = v[i];
210
- if (!e || typeof e !== 'object') {
211
- results.push(failed({ artifact: 'marker', field: `evidence[${i}]`, message: 'must be object', file }));
212
- continue;
213
- }
214
- if (typeof e.scenario_id !== 'string' || !e.scenario_id) {
215
- results.push(failed({
216
- artifact: 'marker',
217
- field: `evidence[${i}].scenario_id`,
218
- message: 'required string',
219
- file,
220
- }));
221
- }
222
- if (typeof e.test_command !== 'string') {
223
- results.push(failed({
224
- artifact: 'marker',
225
- field: `evidence[${i}].test_command`,
226
- message: 'required string',
227
- file,
228
- }));
229
- }
230
- if (typeof e.test_file !== 'string') {
231
- results.push(failed({
232
- artifact: 'marker',
233
- field: `evidence[${i}].test_file`,
234
- message: 'required string',
235
- file,
236
- }));
237
- }
238
- // log_path 必须以 ./ 开头
239
- if (typeof e.log_path !== 'string' || !e.log_path.startsWith('./')) {
68
+ // 5. created_by_tool_version(可选,semver 格式)
69
+ if (m.created_by_tool_version !== undefined) {
70
+ if (typeof m.created_by_tool_version !== 'string') {
240
71
  results.push(failed({
241
72
  artifact: 'marker',
242
- field: `evidence[${i}].log_path`,
243
- message: 'must start with "./"',
73
+ field: 'created_by_tool_version',
74
+ message: 'created_by_tool_version 必须是字符串',
244
75
  file,
245
76
  }));
246
77
  }
247
- if (typeof e.log_hash !== 'string' || !SHA256_RE.test(e.log_hash)) {
78
+ else if (!SEMVER_RE.test(m.created_by_tool_version)) {
248
79
  results.push(failed({
249
80
  artifact: 'marker',
250
- field: `evidence[${i}].log_hash`,
251
- message: 'must match sha256 regex',
252
- file,
253
- }));
254
- }
255
- if (typeof e.pass !== 'boolean') {
256
- results.push(failed({
257
- artifact: 'marker',
258
- field: `evidence[${i}].pass`,
259
- message: 'required boolean',
81
+ field: 'created_by_tool_version',
82
+ message: `created_by_tool_version 必须是 semver(X.Y.Z),实际:${m.created_by_tool_version}`,
260
83
  file,
261
84
  }));
262
85
  }
263
86
  }
264
- return mergeResults(...results);
87
+ return results.length === 0 ? ok() : mergeResults(...results);
265
88
  }
266
- // 校验 git 信息对象
267
- function checkGitInfo(v, file) {
268
- if (!v || typeof v !== 'object') {
269
- return failed({ artifact: 'marker', field: 'git', message: 'must be object', file });
270
- }
271
- const g = v;
272
- if (typeof g.is_git_repo !== 'boolean') {
89
+ /** 校验 ISO 8601 UTC 时间戳字段(允许 ms 精度) */
90
+ function checkTimestamp(obj, field, file) {
91
+ const v = obj[field];
92
+ if (typeof v !== 'string') {
273
93
  return failed({
274
94
  artifact: 'marker',
275
- field: 'git.is_git_repo',
276
- message: 'required boolean',
95
+ field,
96
+ message: `${field} 必须是字符串`,
277
97
  file,
278
98
  });
279
99
  }
280
- // is_git_repo=true 时还需校验 head/diff_hash/diff_pathspec
281
- if (g.is_git_repo) {
282
- if (typeof g.head !== 'string' || !GIT_HEAD_RE.test(g.head)) {
283
- return failed({
284
- artifact: 'marker',
285
- field: 'git.head',
286
- message: 'must match ^[a-f0-9]{40}$',
287
- file,
288
- });
289
- }
290
- if (typeof g.diff_hash !== 'string' || !SHA256_RE.test(g.diff_hash)) {
291
- return failed({
292
- artifact: 'marker',
293
- field: 'git.diff_hash',
294
- message: 'must match sha256',
295
- file,
296
- });
297
- }
298
- if (!g.diff_pathspec || typeof g.diff_pathspec !== 'object') {
299
- return failed({
300
- artifact: 'marker',
301
- field: 'git.diff_pathspec',
302
- message: 'required object',
303
- file,
304
- });
305
- }
306
- // git.diff_pathspec.include / exclude 必须是 string array(spec §3.4.1)
307
- const ps = g.diff_pathspec;
308
- if (!Array.isArray(ps.include) || !ps.include.every((x) => typeof x === 'string')) {
309
- return failed({
310
- artifact: 'marker',
311
- field: 'git.diff_pathspec.include',
312
- message: 'must be array of strings',
313
- file,
314
- });
315
- }
316
- if (!Array.isArray(ps.exclude) || !ps.exclude.every((x) => typeof x === 'string')) {
317
- return failed({
318
- artifact: 'marker',
319
- field: 'git.diff_pathspec.exclude',
320
- message: 'must be array of strings',
321
- file,
322
- });
323
- }
324
- }
325
- return ok();
326
- }
327
- // 校验 review_outcomes 数组
328
- function checkReviewOutcomes(v, file) {
329
- if (!Array.isArray(v)) {
330
- return failed({ artifact: 'marker', field: 'review_outcomes', message: 'must be array', file });
331
- }
332
- const results = [];
333
- for (let i = 0; i < v.length; i++) {
334
- const o = v[i];
335
- if (!o || typeof o !== 'object') {
336
- results.push(failed({
337
- artifact: 'marker',
338
- field: `review_outcomes[${i}]`,
339
- message: 'must be object',
340
- file,
341
- }));
342
- continue;
343
- }
344
- // severity 必须是 6 元素之一(v0.4 simcode S/C/L + v1.0 全名 CRITICAL/WARNING/SUGGESTION;沿 plan-9j v3 BLOCKER 4 双格式扩)
345
- if (typeof o.severity !== 'string' || !REVIEW_OUTCOME_SEVERITY_CODES.has(o.severity)) {
346
- results.push(failed({
347
- artifact: 'marker',
348
- field: `review_outcomes[${i}].severity`,
349
- message: 'must be one of: S / C / L (v0.4 simcode) or CRITICAL / WARNING / SUGGESTION (v1.0 全名)',
350
- file,
351
- }));
352
- }
353
- if (typeof o.accepted !== 'boolean') {
354
- results.push(failed({
355
- artifact: 'marker',
356
- field: `review_outcomes[${i}].accepted`,
357
- message: 'required boolean',
358
- file,
359
- }));
360
- }
361
- if (typeof o.resolved !== 'boolean') {
362
- results.push(failed({
363
- artifact: 'marker',
364
- field: `review_outcomes[${i}].resolved`,
365
- message: 'required boolean',
366
- file,
367
- }));
368
- }
369
- // accepted=true 必须有 task_ref;accepted=false 必须有 rationale
370
- if (o.accepted === true && (typeof o.task_ref !== 'string' || !o.task_ref)) {
371
- results.push(failed({
372
- artifact: 'marker',
373
- field: `review_outcomes[${i}].task_ref`,
374
- message: 'required when accepted=true',
375
- file,
376
- }));
377
- }
378
- if (o.accepted === false && (typeof o.rationale !== 'string' || !o.rationale)) {
379
- results.push(failed({
380
- artifact: 'marker',
381
- field: `review_outcomes[${i}].rationale`,
382
- message: 'required when accepted=false',
383
- file,
384
- }));
385
- }
386
- }
387
- return mergeResults(...results);
388
- }
389
- // plan-9d Task 3:verify_findings 数组校验
390
- // 校验范围:必填字段存在性 + 类型 + 枚举值(沿 master §3.12.1)
391
- // 不校验业务规则(resolved/ack 矩阵 + finding_hash 与 payload 一致性) — 那是 Task 6 fence 的事
392
- function checkVerifyFindingsArray(v, file) {
393
- if (!Array.isArray(v)) {
100
+ if (!ISO_8601_RE.test(v)) {
394
101
  return failed({
395
102
  artifact: 'marker',
396
- field: 'verify_findings',
397
- message: 'must be array',
103
+ field,
104
+ message: `${field} 必须是 ISO 8601 UTC (YYYY-MM-DDTHH:MM:SS[.fff]Z),实际:${v}`,
398
105
  file,
399
106
  });
400
107
  }
401
- const results = [];
402
- // v4 D-2 修订:finding id 唯一性校验(同一 marker 内不允许重复 id,沿 master §3.12.1)
403
- const seenIds = new Set();
404
- for (let i = 0; i < v.length; i++) {
405
- const f = v[i];
406
- if (typeof f?.id === 'number') {
407
- if (seenIds.has(f.id)) {
408
- results.push(failed({
409
- artifact: 'marker',
410
- field: `verify_findings[${i}].id`,
411
- message: `finding id ${f.id} 重复(同一 marker 内 id 必须唯一,沿 master §3.12.1)`,
412
- file,
413
- }));
414
- }
415
- seenIds.add(f.id);
416
- }
417
- }
418
- for (let i = 0; i < v.length; i++) {
419
- const f = v[i];
420
- if (!f || typeof f !== 'object') {
421
- results.push(failed({
422
- artifact: 'marker',
423
- field: `verify_findings[${i}]`,
424
- message: 'must be object',
425
- file,
426
- }));
427
- continue;
428
- }
429
- // 必填 11 字段(沿 master §3.12.1 Finding):
430
- // id / dimension / check_type / severity / automated / evidence / recommendation / resolved
431
- // + finding_hash / content_hash / git_head
432
- if (typeof f.id !== 'number') {
433
- results.push(failed({
434
- artifact: 'marker',
435
- field: `verify_findings[${i}].id`,
436
- message: 'required number',
437
- file,
438
- }));
439
- }
440
- if (typeof f.dimension !== 'string' || !DIMENSION_VALUES.has(f.dimension)) {
441
- results.push(failed({
442
- artifact: 'marker',
443
- field: `verify_findings[${i}].dimension`,
444
- message: 'must be completeness|correctness|coherence|process_evidence',
445
- file,
446
- }));
447
- }
448
- if (typeof f.check_type !== 'string' || !f.check_type) {
449
- results.push(failed({
450
- artifact: 'marker',
451
- field: `verify_findings[${i}].check_type`,
452
- message: 'required string',
453
- file,
454
- }));
455
- }
456
- if (!isSeverity(f.severity)) {
457
- results.push(failed({
458
- artifact: 'marker',
459
- field: `verify_findings[${i}].severity`,
460
- message: `must be one of ${[...SEVERITY_VALUES].join(', ')}`,
461
- file,
462
- }));
463
- }
464
- if (typeof f.automated !== 'boolean') {
465
- results.push(failed({
466
- artifact: 'marker',
467
- field: `verify_findings[${i}].automated`,
468
- message: 'required boolean',
469
- file,
470
- }));
471
- }
472
- if (typeof f.evidence !== 'string' || !f.evidence) {
473
- results.push(failed({
474
- artifact: 'marker',
475
- field: `verify_findings[${i}].evidence`,
476
- message: 'required non-empty string',
477
- file,
478
- }));
479
- }
480
- if (typeof f.recommendation !== 'string' || !f.recommendation) {
481
- results.push(failed({
482
- artifact: 'marker',
483
- field: `verify_findings[${i}].recommendation`,
484
- message: 'required non-empty string',
485
- file,
486
- }));
487
- }
488
- if (typeof f.resolved !== 'boolean') {
489
- results.push(failed({
490
- artifact: 'marker',
491
- field: `verify_findings[${i}].resolved`,
492
- message: 'required boolean',
493
- file,
494
- }));
495
- }
496
- // plan-9d v2 B-1 修订:finding_hash 是裸 64-hex(沿 9a finding-hash.ts:8)
497
- if (typeof f.finding_hash !== 'string' || !FINDING_HASH_RE.test(f.finding_hash)) {
498
- results.push(failed({
499
- artifact: 'marker',
500
- field: `verify_findings[${i}].finding_hash`,
501
- message: 'must match ^[a-f0-9]{64}$ (裸 hex,无 sha256: 前缀,沿 9a)',
502
- file,
503
- }));
504
- }
505
- // content_hash / git_head 是 FindingHashPayload 字段(沿 9a;content_hash sha256: 前缀,git_head 裸 40-hex)
506
- if (typeof f.content_hash !== 'string' || !SHA256_RE.test(f.content_hash)) {
507
- results.push(failed({
508
- artifact: 'marker',
509
- field: `verify_findings[${i}].content_hash`,
510
- message: 'must match ^sha256:[a-f0-9]{64}$',
511
- file,
512
- }));
513
- }
514
- if (typeof f.git_head !== 'string' || !GIT_HEAD_RE.test(f.git_head)) {
515
- results.push(failed({
516
- artifact: 'marker',
517
- field: `verify_findings[${i}].git_head`,
518
- message: 'must match ^[a-f0-9]{40}$',
519
- file,
520
- }));
521
- }
522
- }
523
- return mergeResults(...results);
108
+ return ok();
524
109
  }
525
- // plan-9c Task 1:pause_decisions 数组校验
526
- // 校验范围:必填字段 / 类型 / 枚举值 / id 唯一性(沿 master §3.12.1 + design §2.1.5)
527
- // 不校验业务规则(option=3 必须有 non_blocking_rationale 等) 那是 Task 2 fence 的事
528
- function checkPauseDecisionsArray(v, file) {
529
- if (!Array.isArray(v)) {
110
+ /** 校验 pause_decisions 数组(v4 简化版,无 fence 业务校验) */
111
+ function checkPauseDecisionsArray(value, file) {
112
+ if (!Array.isArray(value)) {
530
113
  return failed({
531
114
  artifact: 'marker',
532
115
  field: 'pause_decisions',
533
- message: 'must be array',
116
+ message: 'pause_decisions 必须是数组(若存在)',
534
117
  file,
535
118
  });
536
119
  }
537
120
  const results = [];
538
- // id 唯一性校验(沿 checkVerifyFindingsArray 同模式)
539
- const seenIds = new Set();
540
- for (let i = 0; i < v.length; i++) {
541
- const p = v[i];
542
- if (typeof p?.id === 'number') {
543
- if (seenIds.has(p.id)) {
544
- results.push(failed({
545
- artifact: 'marker',
546
- field: `pause_decisions[${i}].id`,
547
- message: `pause_decision id ${p.id} 重复(同一 marker 内 id 必须唯一)`,
548
- file,
549
- }));
550
- }
551
- seenIds.add(p.id);
552
- }
553
- }
554
- for (let i = 0; i < v.length; i++) {
555
- const p = v[i];
556
- if (!p || typeof p !== 'object') {
557
- results.push(failed({
558
- artifact: 'marker',
559
- field: `pause_decisions[${i}]`,
560
- message: 'must be object',
561
- file,
562
- }));
121
+ for (let i = 0; i < value.length; i++) {
122
+ const p = value[i];
123
+ const base = `pause_decisions[${i}]`;
124
+ if (!p || typeof p !== 'object' || Array.isArray(p)) {
125
+ results.push(failed({ artifact: 'marker', field: base, message: 'pause_decision 必须是 object', file }));
563
126
  continue;
564
127
  }
565
- const fieldBase = `pause_decisions[${i}]`;
566
- // 必填 8 字段(沿 design §2.1.5,v3 codex NIT 11 修订:7 → 8):id / paused_at / task_ref / issue_summary / severity / chosen_option / target_artifact / target_anchor
567
- if (typeof p.id !== 'number') {
568
- results.push(failed({ artifact: 'marker', field: `${fieldBase}.id`, message: 'required number', file }));
569
- }
128
+ // paused_at
570
129
  if (typeof p.paused_at !== 'string' || !ISO_8601_RE.test(p.paused_at)) {
571
130
  results.push(failed({
572
131
  artifact: 'marker',
573
- field: `${fieldBase}.paused_at`,
574
- message: 'must be ISO 8601 UTC (YYYY-MM-DDTHH:MM:SSZ)',
575
- file,
576
- }));
577
- }
578
- if (typeof p.task_ref !== 'string' || !p.task_ref) {
579
- results.push(failed({
580
- artifact: 'marker',
581
- field: `${fieldBase}.task_ref`,
582
- message: 'required string (e.g., tasks.md#task-3)',
583
- file,
584
- }));
585
- }
586
- if (typeof p.issue_summary !== 'string' || !p.issue_summary) {
587
- results.push(failed({
588
- artifact: 'marker',
589
- field: `${fieldBase}.issue_summary`,
590
- message: 'required non-empty string',
591
- file,
592
- }));
593
- }
594
- if (!isSeverity(p.severity)) {
595
- results.push(failed({
596
- artifact: 'marker',
597
- field: `${fieldBase}.severity`,
598
- message: `must be one of ${[...SEVERITY_VALUES].join(', ')}`,
132
+ field: `${base}.paused_at`,
133
+ message: 'paused_at 必须是 ISO 8601 UTC (YYYY-MM-DDTHH:MM:SS[.fff]Z)',
599
134
  file,
600
135
  }));
601
136
  }
602
- if (typeof p.chosen_option !== 'number' || !CHOSEN_OPTION_VALUES.has(p.chosen_option)) {
603
- results.push(failed({
604
- artifact: 'marker',
605
- field: `${fieldBase}.chosen_option`,
606
- message: 'must be number in {1, 2, 3, 4}',
607
- file,
608
- }));
609
- }
610
- if (typeof p.target_artifact !== 'string' || !p.target_artifact) {
611
- results.push(failed({
612
- artifact: 'marker',
613
- field: `${fieldBase}.target_artifact`,
614
- message: 'required string (e.g., proposal.md / tasks.md / design.md)',
615
- file,
616
- }));
137
+ // task_ref / issue_summary 必填 string
138
+ for (const k of ['task_ref', 'issue_summary']) {
139
+ if (typeof p[k] !== 'string' || p[k].length === 0) {
140
+ results.push(failed({
141
+ artifact: 'marker',
142
+ field: `${base}.${k}`,
143
+ message: `${k} 必须是非空字符串`,
144
+ file,
145
+ }));
146
+ }
617
147
  }
618
- if (typeof p.target_anchor !== 'string' || !p.target_anchor) {
148
+ // chosen_option {1, 2, 3, 4}
149
+ if (typeof p.chosen_option !== 'number' || ![1, 2, 3, 4].includes(p.chosen_option)) {
619
150
  results.push(failed({
620
151
  artifact: 'marker',
621
- field: `${fieldBase}.target_anchor`,
622
- message: 'required string (e.g., "## Out of Scope")',
152
+ field: `${base}.chosen_option`,
153
+ message: 'chosen_option 必须 {1, 2, 3, 4}',
623
154
  file,
624
155
  }));
625
156
  }
626
- // severity_acked_by / severity_acked_at 类型校验(null 或 string)
627
- if (p.severity_acked_by !== null &&
628
- (typeof p.severity_acked_by !== 'string' || !p.severity_acked_by)) {
157
+ // notes 可选 string
158
+ if (p.notes !== undefined && typeof p.notes !== 'string') {
629
159
  results.push(failed({
630
160
  artifact: 'marker',
631
- field: `${fieldBase}.severity_acked_by`,
632
- message: 'must be null or non-empty string',
161
+ field: `${base}.notes`,
162
+ message: 'notes 必须是字符串(若存在)',
633
163
  file,
634
164
  }));
635
165
  }
636
- if (p.severity_acked_at !== null) {
637
- if (typeof p.severity_acked_at !== 'string' || !ISO_8601_RE.test(p.severity_acked_at)) {
638
- results.push(failed({
639
- artifact: 'marker',
640
- field: `${fieldBase}.severity_acked_at`,
641
- message: 'must be null or ISO 8601 UTC',
642
- file,
643
- }));
644
- }
645
- }
646
- // non_blocking_rationale / other_rationale / other_acked_by 类型校验(null 或 string;业务规则在 fence)
647
- for (const field of ['non_blocking_rationale', 'other_rationale', 'other_acked_by']) {
648
- const val = p[field];
649
- if (val !== null && (typeof val !== 'string' || !val)) {
650
- results.push(failed({
651
- artifact: 'marker',
652
- field: `${fieldBase}.${field}`,
653
- message: 'must be null or non-empty string',
654
- file,
655
- }));
656
- }
657
- }
658
- // added_task_ref / capture_id:undefined(老 marker)跳过,
659
- // 存在则须 null 或 non-empty string(schema 层 optional,superset additive)
660
- for (const field of ['added_task_ref', 'capture_id']) {
661
- const val = p[field];
662
- if (val === undefined)
663
- continue; // superset additive:老 marker 缺字段 → 跳过(legacy 分支)
664
- if (val !== null && (typeof val !== 'string' || !val)) {
665
- results.push(failed({
666
- artifact: 'marker',
667
- field: `${fieldBase}.${field}`,
668
- message: 'must be null or non-empty string',
669
- file,
670
- }));
671
- }
672
- }
673
166
  }
674
- return mergeResults(...results);
167
+ return results.length === 0 ? ok() : mergeResults(...results);
675
168
  }
676
169
  //# sourceMappingURL=marker-schema.js.map