project-tiny-context-harness 0.2.70 → 0.2.71
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.
- package/README.md +27 -17
- package/assets/README.md +21 -13
- package/assets/README.zh-CN.md +8 -2
- package/assets/agents/AGENTS_CORE.md +37 -30
- package/assets/skills/context_development_engineer/SKILL.md +14 -9
- package/assets/skills/context_product_plan/SKILL.md +13 -8
- package/assets/skills/context_surface_contract/SKILL.md +27 -19
- package/assets/skills/context_uiux_design/SKILL.md +13 -8
- package/assets/skills/superpowers-long-task/SKILL.md +14 -4
- package/dist/commands/index.js +9 -3
- package/dist/commands/validate.js +1 -1
- package/dist/lib/plan-acceptance-json.d.ts +15 -0
- package/dist/lib/plan-acceptance-json.js +129 -0
- package/dist/lib/plan-acceptance-validator.d.ts +2 -0
- package/dist/lib/plan-acceptance-validator.js +187 -0
- package/dist/lib/plan-contract-validator.d.ts +2 -0
- package/dist/lib/plan-contract-validator.js +127 -0
- package/dist/lib/plan-validator-common.d.ts +24 -0
- package/dist/lib/plan-validator-common.js +196 -0
- package/dist/lib/validators.d.ts +1 -1
- package/dist/lib/validators.js +8 -4
- package/package.json +1 -1
|
@@ -67,12 +67,13 @@ Use when turning audit findings or user decisions into Context candidates.
|
|
|
67
67
|
|
|
68
68
|
Output:
|
|
69
69
|
|
|
70
|
-
- Project-level Product Surface Contract candidate when responsibilities cross surfaces or areas.
|
|
71
|
-
- Area-level Screen Contract candidate when ownership belongs inside one domain.
|
|
72
|
-
- `context.toml` candidate registration with `role = "contract"` when durable registration is needed.
|
|
73
|
-
- `global.md#Context Index` candidate entry when a new Context file is added.
|
|
74
|
-
- Verification candidate for repeatable surface checks.
|
|
75
|
-
-
|
|
70
|
+
- Project-level Product Surface Contract candidate when responsibilities cross surfaces or areas.
|
|
71
|
+
- Area-level Screen Contract candidate when ownership belongs inside one domain.
|
|
72
|
+
- `context.toml` candidate registration with `role = "contract"` when durable registration is needed.
|
|
73
|
+
- `global.md#Context Index` candidate entry when a new Context file is added.
|
|
74
|
+
- Verification candidate for repeatable surface checks.
|
|
75
|
+
- Source-to-Context Coverage candidate when an external product, architecture, technical or acceptance source changes durable surface responsibility.
|
|
76
|
+
- Repo-local Skill task-block candidate when the user wants project-specific enforcement.
|
|
76
77
|
|
|
77
78
|
Do not assume business responsibilities from current code shape alone. Ask for confirmation if the candidate would silently choose between competing product or information-architecture meanings.
|
|
78
79
|
|
|
@@ -104,11 +105,13 @@ Use after implementation or during review.
|
|
|
104
105
|
|
|
105
106
|
Output:
|
|
106
107
|
|
|
107
|
-
- Surface Contract Conformance.
|
|
108
|
-
-
|
|
109
|
-
-
|
|
110
|
-
-
|
|
111
|
-
-
|
|
108
|
+
- Surface Contract Conformance.
|
|
109
|
+
- Source-to-Context Coverage status when a plan surface exists.
|
|
110
|
+
- Context-to-Implementation Binding status when a plan surface exists.
|
|
111
|
+
- Remaining Drift.
|
|
112
|
+
- Missing Context.
|
|
113
|
+
- Implementation Drift.
|
|
114
|
+
- Verification run / not_run / failed.
|
|
112
115
|
|
|
113
116
|
Do not store one-off evidence, screenshots, logs, raw outputs or implementation summaries in Context.
|
|
114
117
|
|
|
@@ -123,7 +126,8 @@ For each touched surface, answer only what is relevant:
|
|
|
123
126
|
- What must move to drilldown, diagnostics, operations, evidence or technical detail?
|
|
124
127
|
- Which long-running or mutating actions require task id, progress, retry, import, recovery or history?
|
|
125
128
|
- Which empty, loading, stale, unavailable, fixture or fallback states matter?
|
|
126
|
-
- What validation path can prove conformance?
|
|
129
|
+
- What validation path can prove conformance?
|
|
130
|
+
- If this came from an external plan/source, which source constraints are covered by existing Context, require new Context, are task-local only, are explicitly out of scope, need user decision or remain under-scoped?
|
|
127
131
|
|
|
128
132
|
## Repo-Local Task Block Candidate
|
|
129
133
|
|
|
@@ -143,11 +147,13 @@ For any task touching user-facing surfaces, information placement, forms, filter
|
|
|
143
147
|
- Main Surface Forbids: `<backend fields, raw payloads, diagnostics, debug ids, fake states, etc.>`
|
|
144
148
|
- Drilldown Ownership: `<details / evidence / operations / diagnostics / technical details>`
|
|
145
149
|
- Long Task State Requirement: `<run id, progress, retry, recovery, import, history, or none>`
|
|
146
|
-
- Context Delta: `<none | required>`
|
|
147
|
-
- Verification: `<view-model test / component test / browser smoke / CLI smoke / manual check>`
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
150
|
+
- Context Delta: `<none | required>`
|
|
151
|
+
- Verification: `<view-model test / component test / browser smoke / CLI smoke / manual check>`
|
|
152
|
+
- Source-to-Context Coverage: `<covered | new_context_required | context_updated | task_local_only | out_of_scope_explicit | needs_user_decision | under_scoped>`
|
|
153
|
+
- Context-to-Implementation Binding: `<bound | partial | missing | blocked | out_of_scope_explicit | needs_user_decision | contradicted_by_current_state>`
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Do not add this task block to package-managed default Skills as a universal gate. Projects opt in through separate project-local Skills.
|
|
151
157
|
|
|
152
158
|
## Implementation Alignment
|
|
153
159
|
|
|
@@ -159,7 +165,9 @@ When implementation is also requested, align code with the Product Surface Contr
|
|
|
159
165
|
- Tests should assert user-facing state semantics, not only backend field plumbing.
|
|
160
166
|
- Browser, app, CLI or game smoke checks should validate actual surface behavior when feasible.
|
|
161
167
|
|
|
162
|
-
Final handoff should include concise `Surface Contract Conformance`: contract source, implementation alignment, remaining drift and verification status.
|
|
168
|
+
Final handoff should include concise `Surface Contract Conformance`: contract source, implementation alignment, remaining drift and verification status.
|
|
169
|
+
|
|
170
|
+
If a `plan.md` or equivalent temporary plan surface exists, conformance must also check its Source-to-Context Coverage and Context-to-Implementation Binding. Remaining `under_scoped` or unresolved `new_context_required` rows mean the implementation cannot be described as fully aligned to the source surface responsibilities. Non-bound surface implementation rows mean it cannot be described as fully aligned to Context; component, modal, viewmodel or unit evidence alone cannot prove main-surface ownership.
|
|
163
171
|
|
|
164
172
|
## Output Boundaries
|
|
165
173
|
|
|
@@ -167,5 +175,5 @@ Final handoff should include concise `Surface Contract Conformance`: contract so
|
|
|
167
175
|
- Do not update Context for ordinary CSS tweaks, copy edits or one-off UI bug fixes unless durable surface responsibility changes.
|
|
168
176
|
- Do not treat current backend fields, enums, JSON, screenshots or terminal output as product intent.
|
|
169
177
|
- Do not invent rationale; rejected alternatives or tradeoffs belong in Context only when they are stable enough to affect future surface decisions.
|
|
170
|
-
- Do not add a validator, edit-order gate or package-level mandatory Surface Contract gate.
|
|
178
|
+
- Do not add a surface-specific validator, edit-order gate or package-level mandatory Surface Contract gate. The generic plan-contract validator may check declared surface binding consistency when a temporary plan surface exists.
|
|
171
179
|
- Do not include business-domain examples in this package-managed Skill.
|
|
@@ -25,7 +25,7 @@ Project-specific UI/UX and visual design rules belong in a separate project-loca
|
|
|
25
25
|
- 若缺失且本任务改变 durable surface responsibility,输出 `Surface Contract Delta: required`,把界面职责写入 `project_context/**`;视觉 token、颜色、字体、间距、圆角和视觉 rationale 仍写入 `DESIGN.md`。
|
|
26
26
|
5. 涉及输入、选择、搜索、筛选、表单/配置、调度/时间窗口、预算/配额/限流或加载/空态/错误态等 UI 控件时,用“控件交互框架”检查控件语义、反馈状态、校验、错误预防、可供性和信息密度;这只是通用判断框架,不是固定控件处方。
|
|
27
27
|
6. 界面职责、流程归属和长期交互契约以 `project_context/**` 为准;`DESIGN.md` 负责视觉 token 和视觉 rationale;代码、截图和搜索结果只说明当前实现状态。Context 决定“应该是什么”,代码和截图揭示“现在是什么”,代码不能静默重定义 Context。
|
|
28
|
-
7. 设计判断或第一处实现编辑前,若任务涉及页面职责、流程边界、信息架构、交互契约、状态或调度语义、可访问性约束、设计验证关键路径或部署关键路径,先编译当前任务契约;契约第一段用 `Context Delta: none|required` 完成唯一正式长期事实判断,再写本次 `Task Contract
|
|
28
|
+
7. 设计判断或第一处实现编辑前,若任务涉及页面职责、流程边界、信息架构、交互契约、状态或调度语义、可访问性约束、设计验证关键路径或部署关键路径,先编译当前任务契约;契约第一段用 `Context Delta: none|required` 完成唯一正式长期事实判断,再写本次 `Task Contract`。如果输入包含产品方案、架构方案、技术方案、界面方案或验收方案,先在 `plan.md` 或等价临时计划面做 Source-to-Context Coverage,确认方案中的 durable surface / IA / interaction / verification constraints 已被现有 Context 或 `DESIGN.md` 覆盖、需要更新、仅属 task-local、显式 out-of-scope、需要用户决策或仍 under-scoped。
|
|
29
29
|
8. 普通 UI bug、局部样式或 CSS 修复、测试修复或探索性 spike 不更新 Context,可先改代码;一旦形成长期交互或视觉结论,继续对齐或交付前必须回写 Context 或 `DESIGN.md`。不要把 Context 机械补成代码改动摘要。
|
|
30
30
|
9. 如果二者冲突,显式标记为实现漂移、缺失工作或 Context 过期。
|
|
31
31
|
10. 如果涉及已有 UI,优先结合代码入口、运行截图或用户提供的参考图来描述差异。
|
|
@@ -46,13 +46,18 @@ Project-specific UI/UX and visual design rules belong in a separate project-loca
|
|
|
46
46
|
- `Context Delta` 必须先出现,取值为 `none` 或 `required`:
|
|
47
47
|
- `none`:本次只是按既有 Context / `DESIGN.md` / 设计原则落地,不新增长期事实。
|
|
48
48
|
- `required`:说明长期事实类型、应写入的 Context / role 或 `DESIGN.md` 位置、需要沉淀的事实,以及明确不写入 Context 的一次性内容。
|
|
49
|
-
- `Task Contract` 用短列表说明页面 / 组件任务、用户判断、主信息和辅助信息归属、动作层级、输入语义、loading / empty / no results / stale / error / degraded / success 状态、布局稳定性、非目标和验收入口。
|
|
50
|
-
- 触及 Product Surface 时,`Task Contract` 同时说明 surface platform、primary user question、main allows/forbids、drilldown ownership、long-task state requirement 和 verification;代码字段、枚举、JSON 或截图只是实现证据,不是产品职责来源。
|
|
51
|
-
- 对长任务、多页面/组件、多 agent
|
|
52
|
-
- `
|
|
53
|
-
- `Context
|
|
54
|
-
- `
|
|
55
|
-
-
|
|
49
|
+
- `Task Contract` 用短列表说明页面 / 组件任务、用户判断、主信息和辅助信息归属、动作层级、输入语义、loading / empty / no results / stale / error / degraded / success 状态、布局稳定性、非目标和验收入口。
|
|
50
|
+
- 触及 Product Surface 时,`Task Contract` 同时说明 surface platform、primary user question、main allows/forbids、drilldown ownership、long-task state requirement 和 verification;代码字段、枚举、JSON 或截图只是实现证据,不是产品职责来源。
|
|
51
|
+
- 对长任务、多页面/组件、多 agent、外部产品/架构/技术/界面/验收方案输入、容易发生 `Context Delta` 调头或多轮截图 / 手动验证的任务,使用 `plan.md` 或等价临时计划面暂存 `Source-to-Context Coverage`、`Context-to-Implementation Binding`、`Context Delta`、`Task Contract`、`Implementation Steps` 和 `Contract Conformance`;它只是临时执行缓存。
|
|
52
|
+
- small code task 指现有 Context / `DESIGN.md` 已足够、且不改变 durable product / architecture / API-schema / runtime-state / verification-deployment / security-redaction / surface ownership 事实的局部实现任务;它按语义风险判断,不按代码行数判断,不应创建 `plan.md`、完整 trace tables、Source-to-Context Coverage 或 Context-to-Implementation Binding,除非它发现长期事实变化或扩展成高风险工作。
|
|
53
|
+
- `Source-to-Context Coverage` 表使用字段:`Source item | Durable constraint | Type | Existing Context Hit | Context action | Owning Context | Coverage status`。这张表只回答 source 约束是否进入或命中 Context / `DESIGN.md`,不写实现路径。
|
|
54
|
+
- `Coverage status` 取值:`covered`、`new_context_required`、`context_updated`、`task_local_only`、`out_of_scope_explicit`、`needs_user_decision`、`under_scoped`。存在 `under_scoped` 或未处理的 `new_context_required` / `needs_user_decision` 时,不能声称已按方案完整实现。
|
|
55
|
+
- `Context-to-Implementation Binding` 表使用字段:`Context fact | Implementation obligation | Expected surfaces | Implemented paths | Forbidden shortcuts | Verification path | Binding status`。
|
|
56
|
+
- `Binding status` 取值:`bound`、`partial`、`missing`、`blocked`、`out_of_scope_explicit`、`needs_user_decision`、`contradicted_by_current_state`。UI/surface 项不能只用 component / viewmodel / mock / unit evidence 冒充 `bound`。
|
|
57
|
+
- `plan.md` 中出现的长期界面、交互或视觉事实必须提炼回 `project_context/**` 或 `DESIGN.md`;否则不要把临时计划当作事实源、交付产物或后续引用依据。
|
|
58
|
+
- `Context Delta: required` 时先更新 `project_context/**` 或 `DESIGN.md`,再继续实现;`none` 时直接按 Task Contract 实现。
|
|
59
|
+
- `Contract Conformance` 是交付前的软检查:实现偏差修实现,契约遗漏回 Task Contract,长期事实缺失或 source coverage under-scoped 回 `Context Delta` 并先更新 Context / `DESIGN.md`。
|
|
60
|
+
- 不为 small code task、普通 UI bug、局部 CSS 修复、小重构、测试修复或探索性 spike 强制编译任务契约。
|
|
56
61
|
|
|
57
62
|
## 信息呈现校准
|
|
58
63
|
|
|
@@ -104,6 +104,7 @@ The prompt must require the future executor to create or initialize `tmp/ty-cont
|
|
|
104
104
|
Each behavior-affecting Technical Realization Plan item must have a trace entry with:
|
|
105
105
|
|
|
106
106
|
- plan item id and plan requirement.
|
|
107
|
+
- acceptance ids covered by the plan item when applicable.
|
|
107
108
|
- expected surfaces.
|
|
108
109
|
- implemented paths.
|
|
109
110
|
- missing paths.
|
|
@@ -112,6 +113,8 @@ Each behavior-affecting Technical Realization Plan item must have a trace entry
|
|
|
112
113
|
- scope assessment.
|
|
113
114
|
- status.
|
|
114
115
|
- drift.
|
|
116
|
+
- Context fact refs when Context Delta is required.
|
|
117
|
+
- For Product Surface, IA or architecture-migration items: conformance type, owner surface, required user paths, forbidden primary surfaces, real page evidence, negative surface checks and default visibility requirement.
|
|
115
118
|
|
|
116
119
|
Allowed plan-conformance statuses:
|
|
117
120
|
|
|
@@ -122,6 +125,7 @@ Allowed plan-conformance statuses:
|
|
|
122
125
|
- `blocked`
|
|
123
126
|
- `scope_changed_requires_user_approval`
|
|
124
127
|
- `contradicted_by_current_state`
|
|
128
|
+
- `out_of_scope_NA`
|
|
125
129
|
|
|
126
130
|
Hard rules:
|
|
127
131
|
|
|
@@ -130,6 +134,7 @@ Hard rules:
|
|
|
130
134
|
- A local audit cannot narrow plan scope or mark completion.
|
|
131
135
|
- Scope correction requires explicit user approval or a revised product/architecture source, Technical Realization Plan and checklist.
|
|
132
136
|
- Every behavior-affecting plan section must have an implementation trace entry.
|
|
137
|
+
- Product Surface, IA or architecture-migration rows cannot be complete without owner surface, required user paths, real page evidence, negative surface checks for forbidden primary surfaces and Context fact refs when Context Delta is required.
|
|
133
138
|
- Any `partial`, `sampled_only`, `not_implemented`, unresolved blocker, unapproved scope change or current contradiction prevents overall done.
|
|
134
139
|
|
|
135
140
|
## Acceptance Evidence Gate
|
|
@@ -139,6 +144,7 @@ The prompt must require the future executor to generate `tmp/ty-context/plan-acc
|
|
|
139
144
|
Each AC verdict entry must include:
|
|
140
145
|
|
|
141
146
|
- AC id or acceptance item.
|
|
147
|
+
- related plan item ids when applicable.
|
|
142
148
|
- status.
|
|
143
149
|
- required evidence.
|
|
144
150
|
- fresh evidence.
|
|
@@ -153,14 +159,18 @@ Allowed AC statuses:
|
|
|
153
159
|
- `blocked`
|
|
154
160
|
- `not_run`
|
|
155
161
|
- `invalidated`
|
|
162
|
+
- `out_of_scope_NA`
|
|
156
163
|
|
|
157
164
|
Hard rules:
|
|
158
165
|
|
|
159
166
|
- Final completion requires an AC-by-AC final acceptance verdict.
|
|
167
|
+
- Before any completion claim, run `ty-context validate-plan-acceptance tmp/ty-context/plan-acceptance/<plan-slug>`; failure prevents final complete and must produce partial / blocker / missing-evidence output.
|
|
168
|
+
- `validate-plan-acceptance` rejects contradictory matrix/verdict JSON, weak-proof complete rows, missing cross-references and declared surface/architecture binding gaps; it checks artifact consistency and references, not product quality.
|
|
160
169
|
- Current API/UI/runtime/data/test contradictions override historical passing evidence.
|
|
161
170
|
- local audit, subagent summaries, final result card text, passing test logs, stale artifacts, partial smoke, dry-run or sampled paths cannot prove completion by themselves.
|
|
162
171
|
- Any current contradiction downgrades the affected AC and overall status.
|
|
163
172
|
- Scope narrowing in audit does not modify acceptance unless the user approved a revised source/plan/checklist.
|
|
173
|
+
- `out_of_scope_NA` requires explicit reason and source reference; arbitrary prose cannot waive missing evidence.
|
|
164
174
|
|
|
165
175
|
## Evidence Layer Separation
|
|
166
176
|
|
|
@@ -220,7 +230,7 @@ Bind the target prompt to the official Skill names and their documented roles:
|
|
|
220
230
|
- Prefer `superpowers:subagent-driven-development` when subagents are available.
|
|
221
231
|
- Use `superpowers:executing-plans` when executing a written plan without the same-session subagent workflow.
|
|
222
232
|
- Plan or AC behavior gap -> TDD: each behavior gap uses `superpowers:test-driven-development` to write a failing test, observe failure, then implement minimally.
|
|
223
|
-
- Before any completion claim, use `superpowers:verification-before-completion` against both `plan-conformance-matrix.*` and `final-acceptance-verdict.*` with fresh evidence
|
|
233
|
+
- Before any completion claim, use `superpowers:verification-before-completion` against both `plan-conformance-matrix.*` and `final-acceptance-verdict.*` with fresh evidence, then run `ty-context validate-plan-acceptance tmp/ty-context/plan-acceptance/<plan-slug>`.
|
|
224
234
|
- review / finish cannot override the plan-conformance matrix or full checklist; if either gate is unsatisfied, continue implementation or report blockers.
|
|
225
235
|
|
|
226
236
|
If Superpowers is missing, install it through the current platform's official Superpowers installation path. If installation is blocked by permissions, network or platform limits, record the blocker in local audit and do not count it as complete.
|
|
@@ -284,7 +294,7 @@ Superpowers 输入包:
|
|
|
284
294
|
7. 每个实现 slice 后更新 matrix 和 audit。
|
|
285
295
|
8. Candidate done 前跑 Plan Conformance Gate:测试通过不等于按图纸完成;sampled path 不等于 full implementation;每个行为 plan item 必须有 code/API/UI/runtime/test/evidence trace。
|
|
286
296
|
9. 再跑 Acceptance Evidence Gate:按验收清单生成 final verdict;current API/UI/runtime/data/test contradiction 高于历史通过记录。
|
|
287
|
-
10. 完成声明前用 superpowers:verification-before-completion
|
|
297
|
+
10. 完成声明前用 superpowers:verification-before-completion 检查 matrix/verdict,并运行 ty-context validate-plan-acceptance tmp/ty-context/plan-acceptance/<plan-slug>;失败就继续或报告 blocker。
|
|
288
298
|
|
|
289
299
|
权限/卡点:在当前平台/仓库/工具/用户已授权权限内最大自主推进;已授权 sudo/gsudo/admin elevation 先尝试,不算用户阻塞。只有本地无法解决的账号/凭证/真实环境/人工审批/敏感字段等才暂停,并给最小用户执行清单(具体页面/系统、字段位置、脱敏/勿发值、拿到后下一步)。
|
|
290
300
|
禁止完成于:local audit、subagent summary、final card、只改代码/计划、只跑部分测试、旧/部分/抽样证据、runtime 未演练、artifact 未被 validator accepted、API/UI 未 reflected、未批准 scope narrowing、任何 API/UI/data/runtime/test 矛盾。
|
|
@@ -318,10 +328,10 @@ Execution order:
|
|
|
318
328
|
7. After each slice, update matrix and audit.
|
|
319
329
|
8. Before candidate done, run Plan Conformance Gate: passing tests does not prove plan conformance; sampled path does not prove full implementation; every behavior plan item needs code/API/UI/runtime/test/evidence trace.
|
|
320
330
|
9. Then run Acceptance Evidence Gate: generate final verdict from the checklist; current API/UI/runtime/data/test contradictions override old passing evidence.
|
|
321
|
-
10. Before completion,
|
|
331
|
+
10. Before completion, run superpowers:verification-before-completion on matrix/verdict and ty-context validate-plan-acceptance tmp/ty-context/plan-acceptance/<plan-slug>; if either fails, continue/report blockers.
|
|
322
332
|
|
|
323
333
|
Autonomy/blockers: within current platform/repo/tool/user-authorized permissions, do all safe self-service discovery/execution/verification. Authorized sudo/gsudo/admin elevation is not a user blocker; try it first. Pause only for locally unsatisfiable account/credential/real-env/human-approval/sensitive-field needs; give exact page/system, field location, redaction/do-not-send values and next agent step.
|
|
324
|
-
Never complete on: local audit, subagent summary, final card, code-only/plan-only work, partial tests, stale/partial/sampled evidence, unexercised runtime, artifact not accepted by validator, API/UI not reflected, unapproved scope narrowing or any API/UI/data/runtime/test contradiction.
|
|
334
|
+
Never complete on: local audit, subagent summary, final card, code-only/plan-only work, partial tests, stale/partial/sampled evidence, unexercised runtime, artifact not accepted by validator, API/UI not reflected, missing validate-plan-acceptance pass, unapproved scope narrowing or any API/UI/data/runtime/test contradiction.
|
|
325
335
|
```
|
|
326
336
|
|
|
327
337
|
Before final response, check the prompt length. If it exceeds 3850 characters, tighten wording while preserving paths, input roles, official Superpowers skill names, Product Context Delta, Technical Context Delta, plan-conformance matrix, final verdict, state machine, UI gate, blockers and invalid evidence.
|
package/dist/commands/index.js
CHANGED
|
@@ -18,6 +18,8 @@ export const commands = {
|
|
|
18
18
|
"validate-context": (args) => validate(["validate-context", ...args]),
|
|
19
19
|
"validate-code-modularity": (args) => validate(["validate-code-modularity", ...args]),
|
|
20
20
|
"validate-harness": (args) => validate(["validate-harness", ...args]),
|
|
21
|
+
"validate-plan-contract": (args) => validate(["validate-plan-contract", ...args]),
|
|
22
|
+
"validate-plan-acceptance": (args) => validate(["validate-plan-acceptance", ...args]),
|
|
21
23
|
package: packageSource
|
|
22
24
|
};
|
|
23
25
|
export function help() {
|
|
@@ -34,8 +36,12 @@ export function help() {
|
|
|
34
36
|
Export temporary Context, code snapshot or bounded Source Pack artifacts
|
|
35
37
|
validate <gate> Run a Harness validation gate
|
|
36
38
|
validate-context Validate Minimal Context fact-source recoverability
|
|
37
|
-
validate-code-modularity
|
|
38
|
-
Enforce touched handwritten source file modularity
|
|
39
|
-
validate-harness Run validate-context and validate-code-modularity
|
|
39
|
+
validate-code-modularity
|
|
40
|
+
Enforce touched handwritten source file modularity
|
|
41
|
+
validate-harness Run validate-context and validate-code-modularity
|
|
42
|
+
validate-plan-contract <plan.md|dir>
|
|
43
|
+
Validate workflow-contract plan surface consistency
|
|
44
|
+
validate-plan-acceptance <dir>
|
|
45
|
+
Validate plan-conformance matrix and final verdict consistency
|
|
40
46
|
package <subcommand> Maintain package canonical source`);
|
|
41
47
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { runValidator } from "../lib/validators.js";
|
|
2
2
|
export async function validate(args) {
|
|
3
3
|
const gate = args[0] ?? "validate-harness";
|
|
4
|
-
const report = await runValidator(process.cwd(), gate);
|
|
4
|
+
const report = await runValidator(process.cwd(), gate, args.slice(1));
|
|
5
5
|
for (const line of report.info) {
|
|
6
6
|
console.log(line);
|
|
7
7
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare const MATRIX_STATUSES: Set<string>;
|
|
2
|
+
export declare const AC_STATUSES: Set<string>;
|
|
3
|
+
export declare const NON_COMPLETE_MATRIX: Set<string>;
|
|
4
|
+
export declare const NON_COMPLETE_AC: Set<string>;
|
|
5
|
+
export declare function findJsonFile(targetDir: string, marker: string): Promise<string | undefined>;
|
|
6
|
+
export declare function readJson(file: string, errors: string[]): Promise<unknown>;
|
|
7
|
+
export declare function findRows(value: unknown, preferredKeys: string[]): Record<string, unknown>[];
|
|
8
|
+
export declare function overallStatus(value: unknown): string;
|
|
9
|
+
export declare function statusOf(row: Record<string, unknown>): string;
|
|
10
|
+
export declare function isOutOfScope(row: Record<string, unknown>): boolean;
|
|
11
|
+
export declare function contextDeltaRequired(value: unknown): boolean;
|
|
12
|
+
export declare function isSurfaceConformanceRow(row: Record<string, unknown>): boolean;
|
|
13
|
+
export declare function hasExplicitNoTestScope(row: Record<string, unknown>): boolean;
|
|
14
|
+
export declare function assertSurfaceConformance(row: Record<string, unknown>, label: string, errors: string[]): void;
|
|
15
|
+
export declare function assertStructuredNa(row: Record<string, unknown>, label: string, errors: string[]): void;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { readText } from "./fs.js";
|
|
4
|
+
import { isBlankish, primitiveText, valuesAsArray } from "./plan-validator-common.js";
|
|
5
|
+
export const MATRIX_STATUSES = new Set([
|
|
6
|
+
"complete",
|
|
7
|
+
"partial",
|
|
8
|
+
"sampled_only",
|
|
9
|
+
"not_implemented",
|
|
10
|
+
"blocked",
|
|
11
|
+
"scope_changed_requires_user_approval",
|
|
12
|
+
"contradicted_by_current_state",
|
|
13
|
+
"out_of_scope_NA"
|
|
14
|
+
]);
|
|
15
|
+
export const AC_STATUSES = new Set(["complete", "partial", "blocked", "not_run", "invalidated", "out_of_scope_NA"]);
|
|
16
|
+
export const NON_COMPLETE_MATRIX = new Set([
|
|
17
|
+
"partial",
|
|
18
|
+
"sampled_only",
|
|
19
|
+
"not_implemented",
|
|
20
|
+
"blocked",
|
|
21
|
+
"scope_changed_requires_user_approval",
|
|
22
|
+
"contradicted_by_current_state"
|
|
23
|
+
]);
|
|
24
|
+
export const NON_COMPLETE_AC = new Set(["partial", "blocked", "not_run", "invalidated"]);
|
|
25
|
+
export async function findJsonFile(targetDir, marker) {
|
|
26
|
+
const entries = await fs.readdir(targetDir, { withFileTypes: true });
|
|
27
|
+
return entries
|
|
28
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith(".json") && entry.name.includes(marker))
|
|
29
|
+
.map((entry) => path.join(targetDir, entry.name))
|
|
30
|
+
.sort()[0];
|
|
31
|
+
}
|
|
32
|
+
export async function readJson(file, errors) {
|
|
33
|
+
try {
|
|
34
|
+
return JSON.parse(await readText(file));
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
errors.push(`${file} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export function findRows(value, preferredKeys) {
|
|
42
|
+
if (Array.isArray(value) && value.every((item) => item && typeof item === "object" && !Array.isArray(item))) {
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
const object = value;
|
|
49
|
+
for (const key of preferredKeys) {
|
|
50
|
+
const rows = findRows(object[key], []);
|
|
51
|
+
if (rows.length > 0) {
|
|
52
|
+
return rows;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return Object.values(object).map((item) => findRows(item, [])).sort((a, b) => b.length - a.length)[0] ?? [];
|
|
56
|
+
}
|
|
57
|
+
export function overallStatus(value) {
|
|
58
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
59
|
+
return "";
|
|
60
|
+
}
|
|
61
|
+
const object = value;
|
|
62
|
+
return String(object.overall_status ?? object.overallStatus ?? object.status ?? "").trim();
|
|
63
|
+
}
|
|
64
|
+
export function statusOf(row) {
|
|
65
|
+
return String(row.status ?? "").trim();
|
|
66
|
+
}
|
|
67
|
+
export function isOutOfScope(row) {
|
|
68
|
+
return statusOf(row) === "out_of_scope_NA" || /out[_ -]?of[_ -]?scope|n\/a|not applicable/i.test(primitiveText(row));
|
|
69
|
+
}
|
|
70
|
+
export function contextDeltaRequired(value) {
|
|
71
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
const object = value;
|
|
75
|
+
return [
|
|
76
|
+
object.context_delta,
|
|
77
|
+
object.contextDelta,
|
|
78
|
+
object.product_context_delta,
|
|
79
|
+
object.productContextDelta,
|
|
80
|
+
object.technical_context_delta,
|
|
81
|
+
object.technicalContextDelta
|
|
82
|
+
].some((item) => /\brequired\b/i.test(primitiveText(item)));
|
|
83
|
+
}
|
|
84
|
+
export function isSurfaceConformanceRow(row) {
|
|
85
|
+
return (/\b(product[_ -]?surface|surface|ia|information[_ -]?architecture|architecture[_ -]?migration|ui)\b/i.test(primitiveText(row.conformance_type)) ||
|
|
86
|
+
!isBlankish(row.owner_surface) ||
|
|
87
|
+
!isBlankish(row.forbidden_primary_surfaces) ||
|
|
88
|
+
!isBlankish(row.required_user_paths));
|
|
89
|
+
}
|
|
90
|
+
export function hasExplicitNoTestScope(row) {
|
|
91
|
+
return /\b(no[- ]?test|no automated test|test out of scope|tests? not required)\b/i.test(primitiveText([row.test_scope, row.no_test_scope, row.tests]));
|
|
92
|
+
}
|
|
93
|
+
export function assertSurfaceConformance(row, label, errors) {
|
|
94
|
+
if (isBlankish(row.owner_surface)) {
|
|
95
|
+
errors.push(`${label} is surface/architecture conformance but owner_surface is empty`);
|
|
96
|
+
}
|
|
97
|
+
if (isBlankish(row.required_user_paths)) {
|
|
98
|
+
errors.push(`${label} is surface/architecture conformance but required_user_paths is empty`);
|
|
99
|
+
}
|
|
100
|
+
if (isBlankish(row.real_page_evidence)) {
|
|
101
|
+
errors.push(`${label} is surface/architecture conformance but real_page_evidence is empty`);
|
|
102
|
+
}
|
|
103
|
+
if (isBlankish(row.context_fact_refs)) {
|
|
104
|
+
errors.push(`${label} is surface/architecture conformance but context_fact_refs is empty`);
|
|
105
|
+
}
|
|
106
|
+
if (!isBlankish(row.forbidden_primary_surfaces) && isBlankish(row.negative_surface_checks)) {
|
|
107
|
+
errors.push(`${label} declares forbidden_primary_surfaces but negative_surface_checks is empty`);
|
|
108
|
+
}
|
|
109
|
+
const userPathText = primitiveText([row.required_user_paths, row.primary_user_paths]);
|
|
110
|
+
for (const forbiddenSurface of valuesAsArray(row.forbidden_primary_surfaces)) {
|
|
111
|
+
if (userPathText.toLowerCase().includes(forbiddenSurface.toLowerCase())) {
|
|
112
|
+
errors.push(`${label} routes a required/primary user path through forbidden surface: ${forbiddenSurface}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (row.default_visibility_required === true && !mentionsDefaultVisibility(primitiveText(row.real_page_evidence))) {
|
|
116
|
+
errors.push(`${label} requires default visibility but real_page_evidence does not record default-visible proof`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
export function assertStructuredNa(row, label, errors) {
|
|
120
|
+
if (isBlankish(row.na_reason) && isBlankish(row.out_of_scope_reason)) {
|
|
121
|
+
errors.push(`${label} is out_of_scope_NA but lacks na_reason or out_of_scope_reason`);
|
|
122
|
+
}
|
|
123
|
+
if (isBlankish(row.scope_source) && isBlankish(row.approval_source) && isBlankish(row.source_reference)) {
|
|
124
|
+
errors.push(`${label} is out_of_scope_NA but lacks scope_source, approval_source or source_reference`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function mentionsDefaultVisibility(value) {
|
|
128
|
+
return /\b(default[- ]?visible|visible by default|primary entry|first-level|top-level|main entry)\b/i.test(value);
|
|
129
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { pathExists } from "./fs.js";
|
|
2
|
+
import { AC_STATUSES, MATRIX_STATUSES, NON_COMPLETE_AC, NON_COMPLETE_MATRIX, assertStructuredNa, assertSurfaceConformance, contextDeltaRequired, findJsonFile, findRows, hasExplicitNoTestScope, isOutOfScope, isSurfaceConformanceRow, overallStatus, readJson, statusOf } from "./plan-acceptance-json.js";
|
|
3
|
+
import { assertReferencedPathsExist, hasRealPageEvidence, isBlankish, isUiFacing, primitiveText, repoRelative, resolveInputDir, valuesAsArray, weakProofHit } from "./plan-validator-common.js";
|
|
4
|
+
export async function validatePlanAcceptance(projectRoot, args = []) {
|
|
5
|
+
const info = [];
|
|
6
|
+
const errors = [];
|
|
7
|
+
const targetDir = await resolveInputDir(projectRoot, args[0], "tmp/ty-context/plan-acceptance");
|
|
8
|
+
if (!(await pathExists(targetDir))) {
|
|
9
|
+
return { info, errors: [`plan acceptance directory is missing: ${repoRelative(projectRoot, targetDir)}`] };
|
|
10
|
+
}
|
|
11
|
+
const matrixFile = await findJsonFile(targetDir, "plan-conformance-matrix");
|
|
12
|
+
const verdictFile = await findJsonFile(targetDir, "final-acceptance-verdict");
|
|
13
|
+
if (!matrixFile) {
|
|
14
|
+
errors.push(`plan acceptance directory is missing *-plan-conformance-matrix.json`);
|
|
15
|
+
}
|
|
16
|
+
if (!verdictFile) {
|
|
17
|
+
errors.push(`plan acceptance directory is missing *-final-acceptance-verdict.json`);
|
|
18
|
+
}
|
|
19
|
+
if (!matrixFile || !verdictFile) {
|
|
20
|
+
return { info, errors };
|
|
21
|
+
}
|
|
22
|
+
const matrix = await readJson(matrixFile, errors);
|
|
23
|
+
const verdict = await readJson(verdictFile, errors);
|
|
24
|
+
if (matrix === undefined || verdict === undefined) {
|
|
25
|
+
return { info, errors };
|
|
26
|
+
}
|
|
27
|
+
const matrixRows = findRows(matrix, ["plan_items", "items", "matrix", "entries", "plan_conformance"]);
|
|
28
|
+
const verdictRows = findRows(verdict, ["acceptance_items", "ac_verdicts", "verdicts", "items", "entries", "acs"]);
|
|
29
|
+
await validateMatrixRows(projectRoot, matrixRows, overallStatus(matrix), errors);
|
|
30
|
+
await validateVerdictRows(projectRoot, verdictRows, overallStatus(verdict), errors);
|
|
31
|
+
validateCrossReferences(matrixRows, verdictRows, errors);
|
|
32
|
+
validateContextFactReferences(matrix, verdict, matrixRows, verdictRows, errors);
|
|
33
|
+
info.push(`checked plan acceptance ${repoRelative(projectRoot, targetDir)} matrix_rows=${matrixRows.length} verdict_rows=${verdictRows.length}`);
|
|
34
|
+
if (errors.length === 0) {
|
|
35
|
+
info.push("Plan acceptance validation passed");
|
|
36
|
+
}
|
|
37
|
+
return { info, errors };
|
|
38
|
+
}
|
|
39
|
+
async function validateMatrixRows(projectRoot, rows, overall, errors) {
|
|
40
|
+
if (rows.length === 0) {
|
|
41
|
+
errors.push("plan-conformance matrix has no trace rows");
|
|
42
|
+
}
|
|
43
|
+
for (const [index, row] of rows.entries()) {
|
|
44
|
+
const label = `plan-conformance matrix row ${index + 1}`;
|
|
45
|
+
const status = statusOf(row);
|
|
46
|
+
if (!MATRIX_STATUSES.has(status)) {
|
|
47
|
+
errors.push(`${label} has unsupported status: ${status || "<empty>"}`);
|
|
48
|
+
}
|
|
49
|
+
if (status === "out_of_scope_NA") {
|
|
50
|
+
assertStructuredNa(row, label, errors);
|
|
51
|
+
}
|
|
52
|
+
if (overall === "complete" && NON_COMPLETE_MATRIX.has(status)) {
|
|
53
|
+
errors.push(`${label} is ${status} but overall_status is complete`);
|
|
54
|
+
}
|
|
55
|
+
if (status === "complete" && !isBlankish(row.missing_paths)) {
|
|
56
|
+
errors.push(`${label} is complete but missing_paths is not empty`);
|
|
57
|
+
}
|
|
58
|
+
if (status === "complete") {
|
|
59
|
+
if (isBlankish(row.plan_requirement)) {
|
|
60
|
+
errors.push(`${label} is complete but plan_requirement is empty`);
|
|
61
|
+
}
|
|
62
|
+
if (isBlankish(row.expected_surfaces)) {
|
|
63
|
+
errors.push(`${label} is complete but expected_surfaces is empty`);
|
|
64
|
+
}
|
|
65
|
+
if (isBlankish(row.implemented_paths)) {
|
|
66
|
+
errors.push(`${label} is complete but implemented_paths is empty`);
|
|
67
|
+
}
|
|
68
|
+
if (isBlankish(row.tests) && !hasExplicitNoTestScope(row)) {
|
|
69
|
+
errors.push(`${label} is complete but tests is empty and no explicit no-test scope is recorded`);
|
|
70
|
+
}
|
|
71
|
+
if (isBlankish(row.runtime_evidence) && isBlankish(row.artifact_evidence) && isBlankish(row.real_page_evidence)) {
|
|
72
|
+
errors.push(`${label} is complete but has no runtime, artifact or real-page evidence`);
|
|
73
|
+
}
|
|
74
|
+
if (isBlankish(row.scope_assessment)) {
|
|
75
|
+
errors.push(`${label} is complete but scope_assessment is empty`);
|
|
76
|
+
}
|
|
77
|
+
if (isBlankish(row.drift)) {
|
|
78
|
+
errors.push(`${label} is complete but drift is empty`);
|
|
79
|
+
}
|
|
80
|
+
const weak = weakProofHit(primitiveText(row));
|
|
81
|
+
if (weak) {
|
|
82
|
+
errors.push(`${label} is complete but contains weak-proof language matching /${weak}/`);
|
|
83
|
+
}
|
|
84
|
+
if (isUiFacing(primitiveText([row.expected_surfaces, row.plan_requirement, row.conformance_type]))) {
|
|
85
|
+
const realPageEvidence = primitiveText([
|
|
86
|
+
row.real_page_evidence,
|
|
87
|
+
row.user_path_evidence,
|
|
88
|
+
row.fresh_evidence,
|
|
89
|
+
row.runtime_evidence,
|
|
90
|
+
row.artifact_evidence
|
|
91
|
+
]);
|
|
92
|
+
if (!hasRealPageEvidence(realPageEvidence)) {
|
|
93
|
+
errors.push(`${label} is UI/surface-facing but lacks real_page_evidence`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (isSurfaceConformanceRow(row)) {
|
|
97
|
+
assertSurfaceConformance(row, label, errors);
|
|
98
|
+
}
|
|
99
|
+
await assertReferencedPathsExist(projectRoot, label, primitiveText([
|
|
100
|
+
row.implemented_paths,
|
|
101
|
+
row.tests,
|
|
102
|
+
row.runtime_evidence,
|
|
103
|
+
row.artifact_evidence,
|
|
104
|
+
row.real_page_evidence,
|
|
105
|
+
row.negative_surface_checks,
|
|
106
|
+
row.context_fact_refs
|
|
107
|
+
]), errors);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
async function validateVerdictRows(projectRoot, rows, overall, errors) {
|
|
112
|
+
if (rows.length === 0) {
|
|
113
|
+
errors.push("final acceptance verdict has no AC rows");
|
|
114
|
+
}
|
|
115
|
+
for (const [index, row] of rows.entries()) {
|
|
116
|
+
const label = `final acceptance verdict row ${index + 1}`;
|
|
117
|
+
const status = statusOf(row);
|
|
118
|
+
if (!AC_STATUSES.has(status)) {
|
|
119
|
+
errors.push(`${label} has unsupported status: ${status || "<empty>"}`);
|
|
120
|
+
}
|
|
121
|
+
if (status === "out_of_scope_NA") {
|
|
122
|
+
assertStructuredNa(row, label, errors);
|
|
123
|
+
}
|
|
124
|
+
if (overall === "complete" && NON_COMPLETE_AC.has(status)) {
|
|
125
|
+
errors.push(`${label} is ${status} but overall_status is complete`);
|
|
126
|
+
}
|
|
127
|
+
if (status === "complete" && isBlankish(row.fresh_evidence)) {
|
|
128
|
+
errors.push(`${label} is complete but fresh_evidence is empty`);
|
|
129
|
+
}
|
|
130
|
+
if (status === "complete" && isBlankish(row.required_evidence)) {
|
|
131
|
+
errors.push(`${label} is complete but required_evidence is empty`);
|
|
132
|
+
}
|
|
133
|
+
if (status === "complete" && isBlankish(row.decision)) {
|
|
134
|
+
errors.push(`${label} is complete but decision is empty`);
|
|
135
|
+
}
|
|
136
|
+
if (status === "complete" && !isBlankish(row.missing_evidence)) {
|
|
137
|
+
errors.push(`${label} is complete but missing_evidence is not empty`);
|
|
138
|
+
}
|
|
139
|
+
if (status === "complete" && !isBlankish(row.contradictions)) {
|
|
140
|
+
errors.push(`${label} is complete but contradictions is not empty`);
|
|
141
|
+
}
|
|
142
|
+
if (status === "complete") {
|
|
143
|
+
const text = primitiveText(row);
|
|
144
|
+
const weak = weakProofHit(text);
|
|
145
|
+
if (weak) {
|
|
146
|
+
errors.push(`${label} is complete but contains weak-proof language matching /${weak}/`);
|
|
147
|
+
}
|
|
148
|
+
if (isUiFacing(text) && !isOutOfScope(row) && !hasRealPageEvidence(primitiveText(row.fresh_evidence))) {
|
|
149
|
+
errors.push(`${label} is UI-facing but lacks fresh real-page evidence or explicit N/A`);
|
|
150
|
+
}
|
|
151
|
+
await assertReferencedPathsExist(projectRoot, label, primitiveText([row.fresh_evidence, row.context_fact_refs]), errors);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
function validateCrossReferences(matrixRows, verdictRows, errors) {
|
|
156
|
+
const planIds = new Set(matrixRows.map((row) => String(row.plan_item_id ?? row.id ?? "")).filter(Boolean));
|
|
157
|
+
const acIds = new Set(verdictRows.map((row) => String(row.ac_id ?? row.id ?? row.acceptance_item ?? "")).filter(Boolean));
|
|
158
|
+
let checked = 0;
|
|
159
|
+
for (const [index, row] of matrixRows.entries()) {
|
|
160
|
+
for (const acId of valuesAsArray(row.acceptance_ids ?? row.ac_ids)) {
|
|
161
|
+
checked += 1;
|
|
162
|
+
if (!acIds.has(acId)) {
|
|
163
|
+
errors.push(`plan-conformance matrix row ${index + 1} references unknown AC id: ${acId}`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
for (const [index, row] of verdictRows.entries()) {
|
|
168
|
+
for (const planId of valuesAsArray(row.related_plan_item_ids ?? row.plan_item_ids)) {
|
|
169
|
+
checked += 1;
|
|
170
|
+
if (!planIds.has(planId)) {
|
|
171
|
+
errors.push(`final acceptance verdict row ${index + 1} references unknown plan item id: ${planId}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (matrixRows.length > 0 && verdictRows.length > 0 && checked === 0) {
|
|
176
|
+
errors.push("plan acceptance artifacts must include acceptance_ids or related_plan_item_ids cross references");
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
function validateContextFactReferences(matrix, verdict, matrixRows, verdictRows, errors) {
|
|
180
|
+
if (!contextDeltaRequired(matrix) && !contextDeltaRequired(verdict)) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
const rows = [...matrixRows, ...verdictRows];
|
|
184
|
+
if (!rows.some((row) => !isBlankish(row.context_fact_refs))) {
|
|
185
|
+
errors.push("Context Delta is required but matrix/verdict rows do not cite context_fact_refs");
|
|
186
|
+
}
|
|
187
|
+
}
|