@tiic-tech/openworkflow 0.1.1 → 0.1.2
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/dist/adapters/codex/src/cleanCodexAdapter.js +4 -1
- package/dist/adapters/codex/src/cleanCodexAdapter.js.map +1 -1
- package/dist/adapters/codex/src/constants.d.ts +1 -0
- package/dist/adapters/codex/src/constants.js +2 -0
- package/dist/adapters/codex/src/constants.js.map +1 -0
- package/dist/adapters/codex/src/generateCommands.js +14 -9
- package/dist/adapters/codex/src/generateCommands.js.map +1 -1
- package/dist/adapters/codex/src/generateSkills.js +13 -0
- package/dist/adapters/codex/src/generateSkills.js.map +1 -1
- package/dist/adapters/codex/src/generatedFiles.js +1 -1
- package/dist/adapters/codex/src/manifest.js +11 -2
- package/dist/adapters/codex/src/manifest.js.map +1 -1
- package/dist/adapters/codex/src/templates.d.ts +0 -1
- package/dist/adapters/codex/src/templates.js +0 -1
- package/dist/adapters/codex/src/templates.js.map +1 -1
- package/dist/adapters/src/registry.d.ts +20 -0
- package/dist/adapters/src/registry.js +81 -0
- package/dist/adapters/src/registry.js.map +1 -0
- package/dist/cli/src/commands/brief.d.ts +46 -0
- package/dist/cli/src/commands/brief.js +294 -0
- package/dist/cli/src/commands/brief.js.map +1 -0
- package/dist/cli/src/commands/check.d.ts +42 -0
- package/dist/cli/src/commands/check.js +326 -0
- package/dist/cli/src/commands/check.js.map +1 -0
- package/dist/cli/src/commands/clean.js +46 -1
- package/dist/cli/src/commands/clean.js.map +1 -1
- package/dist/cli/src/commands/context.d.ts +1 -0
- package/dist/cli/src/commands/context.js +471 -0
- package/dist/cli/src/commands/context.js.map +1 -0
- package/dist/cli/src/commands/doctor.js +122 -12
- package/dist/cli/src/commands/doctor.js.map +1 -1
- package/dist/cli/src/commands/draft.d.ts +1 -0
- package/dist/cli/src/commands/draft.js +175 -0
- package/dist/cli/src/commands/draft.js.map +1 -0
- package/dist/cli/src/commands/gitAutomation.d.ts +1 -0
- package/dist/cli/src/commands/gitAutomation.js +378 -0
- package/dist/cli/src/commands/gitAutomation.js.map +1 -0
- package/dist/cli/src/commands/handoff.d.ts +22 -0
- package/dist/cli/src/commands/handoff.js +122 -0
- package/dist/cli/src/commands/handoff.js.map +1 -0
- package/dist/cli/src/commands/init.js +52 -1
- package/dist/cli/src/commands/init.js.map +1 -1
- package/dist/cli/src/commands/inspect.d.ts +23 -0
- package/dist/cli/src/commands/inspect.js +157 -0
- package/dist/cli/src/commands/inspect.js.map +1 -0
- package/dist/cli/src/commands/register.d.ts +1 -0
- package/dist/cli/src/commands/register.js +251 -0
- package/dist/cli/src/commands/register.js.map +1 -0
- package/dist/cli/src/commands/resume.d.ts +59 -0
- package/dist/cli/src/commands/resume.js +280 -0
- package/dist/cli/src/commands/resume.js.map +1 -0
- package/dist/cli/src/commands/shared.js +6 -2
- package/dist/cli/src/commands/shared.js.map +1 -1
- package/dist/cli/src/commands/summaries.d.ts +1 -0
- package/dist/cli/src/commands/summaries.js +77 -0
- package/dist/cli/src/commands/summaries.js.map +1 -0
- package/dist/cli/src/commands/summarize.d.ts +1 -0
- package/dist/cli/src/commands/summarize.js +316 -0
- package/dist/cli/src/commands/summarize.js.map +1 -0
- package/dist/cli/src/commands/sync.js +135 -12
- package/dist/cli/src/commands/sync.js.map +1 -1
- package/dist/cli/src/commands/validate.js +25 -1
- package/dist/cli/src/commands/validate.js.map +1 -1
- package/dist/cli/src/dev/verifyAgentE2E.d.ts +2 -0
- package/dist/cli/src/dev/verifyAgentE2E.js +391 -0
- package/dist/cli/src/dev/verifyAgentE2E.js.map +1 -0
- package/dist/cli/src/dev/verifyCleanCommand.js +213 -2
- package/dist/cli/src/dev/verifyCleanCommand.js.map +1 -1
- package/dist/cli/src/dev/verifyRuntimeSurface.js +4940 -54
- package/dist/cli/src/dev/verifyRuntimeSurface.js.map +1 -1
- package/dist/cli/src/dev/verifyWorkflowE2E.js +477 -45
- package/dist/cli/src/dev/verifyWorkflowE2E.js.map +1 -1
- package/dist/cli/src/index.js +184 -6
- package/dist/cli/src/index.js.map +1 -1
- package/dist/cli/src/report.d.ts +26 -0
- package/dist/cli/src/report.js +17 -0
- package/dist/cli/src/report.js.map +1 -0
- package/dist/core/src/artifacts/readiness.d.ts +7 -0
- package/dist/core/src/artifacts/readiness.js +240 -0
- package/dist/core/src/artifacts/readiness.js.map +1 -0
- package/dist/core/src/artifacts/registry.d.ts +9 -2
- package/dist/core/src/artifacts/registry.js +687 -60
- package/dist/core/src/artifacts/registry.js.map +1 -1
- package/dist/core/src/commands/registry.js +1425 -146
- package/dist/core/src/commands/registry.js.map +1 -1
- package/dist/core/src/contracts/index.d.ts +1 -1
- package/dist/core/src/fs/index.d.ts +24 -0
- package/dist/core/src/fs/index.js +48 -1
- package/dist/core/src/fs/index.js.map +1 -1
- package/dist/core/src/git/autonomousSimulator.d.ts +46 -0
- package/dist/core/src/git/autonomousSimulator.js +163 -0
- package/dist/core/src/git/autonomousSimulator.js.map +1 -0
- package/dist/core/src/git/branchIdentity.d.ts +19 -0
- package/dist/core/src/git/branchIdentity.js +75 -0
- package/dist/core/src/git/branchIdentity.js.map +1 -0
- package/dist/core/src/git/draftPrPilot.d.ts +47 -0
- package/dist/core/src/git/draftPrPilot.js +196 -0
- package/dist/core/src/git/draftPrPilot.js.map +1 -0
- package/dist/core/src/git/localEvidenceReader.d.ts +21 -0
- package/dist/core/src/git/localEvidenceReader.js +142 -0
- package/dist/core/src/git/localEvidenceReader.js.map +1 -0
- package/dist/core/src/git/localGitAutomation.d.ts +68 -0
- package/dist/core/src/git/localGitAutomation.js +470 -0
- package/dist/core/src/git/localGitAutomation.js.map +1 -0
- package/dist/core/src/git/mergeReadinessCheckpoint.d.ts +31 -0
- package/dist/core/src/git/mergeReadinessCheckpoint.js +110 -0
- package/dist/core/src/git/mergeReadinessCheckpoint.js.map +1 -0
- package/dist/core/src/git/prReadySummary.d.ts +16 -0
- package/dist/core/src/git/prReadySummary.js +144 -0
- package/dist/core/src/git/prReadySummary.js.map +1 -0
- package/dist/core/src/git/remoteReadonlyPlanner.d.ts +60 -0
- package/dist/core/src/git/remoteReadonlyPlanner.js +223 -0
- package/dist/core/src/git/remoteReadonlyPlanner.js.map +1 -0
- package/dist/core/src/onboarding/agentsGuide.d.ts +32 -0
- package/dist/core/src/onboarding/agentsGuide.js +164 -0
- package/dist/core/src/onboarding/agentsGuide.js.map +1 -0
- package/dist/core/src/validators/validateOpenWorkflow.js +1331 -15
- package/dist/core/src/validators/validateOpenWorkflow.js.map +1 -1
- package/dist/core/src/validators/validateRepositoryContracts.js +2321 -306
- package/dist/core/src/validators/validateRepositoryContracts.js.map +1 -1
- package/dist/core/src/workflow/cleanOpenWorkflow.d.ts +2 -0
- package/dist/core/src/workflow/cleanOpenWorkflow.js +97 -8
- package/dist/core/src/workflow/cleanOpenWorkflow.js.map +1 -1
- package/dist/core/src/workflow/doctorOpenWorkflow.d.ts +7 -0
- package/dist/core/src/workflow/doctorOpenWorkflow.js +26 -0
- package/dist/core/src/workflow/doctorOpenWorkflow.js.map +1 -0
- package/dist/core/src/workflow/initOpenWorkflow.d.ts +7 -0
- package/dist/core/src/workflow/initOpenWorkflow.js +96 -8
- package/dist/core/src/workflow/initOpenWorkflow.js.map +1 -1
- package/dist/core/src/workflow/planningQueueResume.d.ts +105 -0
- package/dist/core/src/workflow/planningQueueResume.js +596 -0
- package/dist/core/src/workflow/planningQueueResume.js.map +1 -0
- package/dist/core/src/workflow/readWorkflowConfig.d.ts +6 -0
- package/dist/core/src/workflow/readWorkflowConfig.js +28 -0
- package/dist/core/src/workflow/readWorkflowConfig.js.map +1 -0
- package/dist/core/src/workflow/summaryHealth.d.ts +60 -0
- package/dist/core/src/workflow/summaryHealth.js +713 -0
- package/dist/core/src/workflow/summaryHealth.js.map +1 -0
- package/dist/core/src/workflow/syncOpenWorkflow.d.ts +22 -0
- package/dist/core/src/workflow/syncOpenWorkflow.js +235 -0
- package/dist/core/src/workflow/syncOpenWorkflow.js.map +1 -0
- package/package.json +2 -1
- package/references/artifact-authoring-templates.md +14 -12
- package/references/artifact-instruction-envelope.md +133 -0
- package/references/coder-continuous-growth-loop.md +68 -0
- package/references/gh-operation-governance.md +114 -0
- package/references/git-automation-governance.md +324 -0
- package/references/git-version-control-governance.md +227 -0
- package/references/internal-coder-protocol.md +202 -0
- package/references/issue-governance.md +115 -0
- package/references/planning-artifact-contracts.md +595 -0
- package/references/planning-skill-runtime-exposure.md +159 -0
- package/references/proto-redesign-artifact-contracts.md +217 -0
- package/references/proto2html-artifact-contracts.md +113 -0
- package/references/skill-system-lifecycle.md +198 -0
- package/references/validation-trust-domains.md +286 -0
- package/references/workflow-blueprint-runtime-alignment.md +287 -0
- package/schemas/atom-tasks.schema.json +101 -0
- package/schemas/candidate-changes.schema.json +323 -0
- package/schemas/current-state.schema.json +113 -0
- package/schemas/html-prototype.schema.json +288 -0
- package/schemas/openworkflow-contract.schema.json +9 -1
- package/schemas/proto-prompt-pack.schema.json +1333 -0
- package/schemas/prototype-evidence.schema.json +684 -142
- package/schemas/selected-change.schema.json +104 -0
- package/schemas/validation-target.schema.json +187 -1
- package/schemas/validation.schema.json +187 -1
- package/schemas/vision-session.schema.json +151 -0
- package/skills/analyze-changes/SKILL.md +92 -0
- package/skills/analyze-changes/agents/openai.yaml +4 -0
- package/skills/analyze-changes/references/analysis-protocol.md +116 -0
- package/skills/build-proto-prompt/SKILL.md +125 -0
- package/skills/build-proto-prompt/references/output-boundary.md +54 -0
- package/skills/build-proto-prompt/references/prompt-pack-compiler-protocol.md +80 -0
- package/skills/build-prototype/SKILL.md +162 -38
- package/skills/build-prototype/agents/openai.yaml +2 -2
- package/skills/build-prototype/references/philosophy-engine.md +61 -0
- package/skills/build-prototype/references/strategic-prompt-pack-protocol.md +365 -0
- package/skills/build-prototype/references/vision2prompt/01_input_contract.md +84 -0
- package/skills/build-prototype/references/vision2prompt/02_vision_decomposition.md +108 -0
- package/skills/build-prototype/references/vision2prompt/03_strategy_hypothesis_generation.md +89 -0
- package/skills/build-prototype/references/vision2prompt/04_product_system_extraction.md +78 -0
- package/skills/build-prototype/references/vision2prompt/05_prototype_prompt_schema.md +189 -0
- package/skills/build-prototype/references/vision2prompt/06_output_templates.md +125 -0
- package/skills/build-prototype/references/vision2prompt/07_quality_rubric.md +171 -0
- package/skills/build-validation/SKILL.md +136 -54
- package/skills/build-validation/references/prototype-validation-target-rubric.md +35 -0
- package/skills/build-validation/references/return-to-vision-gate.md +32 -0
- package/skills/build-vision/SKILL.md +192 -0
- package/skills/build-vision/references/proto-readiness-rubric.md +48 -0
- package/skills/build-vision/references/vision-interview-protocol.md +48 -0
- package/skills/coder/SKILL.md +204 -0
- package/skills/decompose-to-changes/SKILL.md +176 -0
- package/skills/decompose-to-changes/agents/openai.yaml +4 -0
- package/skills/decompose-to-changes/references/decomposition-protocol.md +278 -0
- package/skills/prompt2proto/SKILL.md +157 -0
- package/skills/prompt2proto/agents/openai.yaml +4 -0
- package/skills/prompt2proto/references/00_role_philosophy_engine.md +96 -0
- package/skills/prompt2proto/references/01_input_contract.md +53 -0
- package/skills/prompt2proto/references/02_prompt_pack_readiness.md +50 -0
- package/skills/prompt2proto/references/03_visual_translation_workflow.md +64 -0
- package/skills/prompt2proto/references/04_output_contract.md +67 -0
- package/skills/prompt2proto/references/05_quality_rubric.md +46 -0
- package/skills/proto2html/SKILL.md +136 -0
- package/skills/proto2html/agents/openai.yaml +4 -0
- package/skills/proto2html/references/proto2html-protocol.md +115 -0
- package/skills/run-team/SKILL.md +4 -0
- package/skills/select-change/SKILL.md +200 -0
- package/skills/select-change/agents/openai.yaml +4 -0
- package/skills/select-change/references/selection-protocol.md +281 -0
- package/skills/tune-prototype/SKILL.md +121 -0
- package/skills/tune-prototype/agents/openai.yaml +4 -0
- package/skills/tune-prototype/references/refined-prompt-pack-protocol.md +161 -0
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { readdir, readFile, stat } from "node:fs/promises";
|
|
2
2
|
import { statSync } from "node:fs";
|
|
3
3
|
import { basename, join, relative, resolve } from "node:path";
|
|
4
|
+
import { getWorkflowCommands } from "../commands/registry.js";
|
|
4
5
|
import { SCHEMA_VERSION } from "../contracts/index.js";
|
|
5
6
|
import { parseYaml } from "../contracts/yaml.js";
|
|
6
|
-
import { isNotFound } from "../fs/index.js";
|
|
7
|
+
import { isExternalReference, isNotFound, resolveLocalReference } from "../fs/index.js";
|
|
8
|
+
import { assessBranchIdentity, branchIdentityExceptionFrom } from "../git/branchIdentity.js";
|
|
7
9
|
const REQUIRED_FILES = [
|
|
8
10
|
"AGENT.md",
|
|
9
11
|
"README.md",
|
|
@@ -16,7 +18,9 @@ const REQUIRED_FILES = [
|
|
|
16
18
|
"references/discovery-artifact-contracts.md",
|
|
17
19
|
"references/artifact-authoring-templates.md",
|
|
18
20
|
"references/runtime-command-surface.md",
|
|
21
|
+
"references/proto2html-artifact-contracts.md",
|
|
19
22
|
"schemas/openworkflow-contract.schema.json",
|
|
23
|
+
"schemas/current-state.schema.json",
|
|
20
24
|
"schemas/workflow-index.schema.json",
|
|
21
25
|
"schemas/contract-graph.schema.json",
|
|
22
26
|
"schemas/artifact-contracts.schema.json",
|
|
@@ -24,6 +28,7 @@ const REQUIRED_FILES = [
|
|
|
24
28
|
"schemas/vision-session.schema.json",
|
|
25
29
|
"schemas/validation-target.schema.json",
|
|
26
30
|
"schemas/prototype-evidence.schema.json",
|
|
31
|
+
"schemas/html-prototype.schema.json",
|
|
27
32
|
"schemas/decision-record.schema.json",
|
|
28
33
|
"schemas/product-design.schema.json",
|
|
29
34
|
"schemas/change.schema.json",
|
|
@@ -33,22 +38,39 @@ const REQUIRED_FILES = [
|
|
|
33
38
|
"package.json",
|
|
34
39
|
"tsconfig.json",
|
|
35
40
|
"packages/cli/src/index.ts",
|
|
41
|
+
"packages/cli/src/report.ts",
|
|
42
|
+
"packages/cli/src/commands/brief.ts",
|
|
43
|
+
"packages/cli/src/commands/check.ts",
|
|
36
44
|
"packages/cli/src/commands/clean.ts",
|
|
45
|
+
"packages/cli/src/commands/context.ts",
|
|
46
|
+
"packages/cli/src/commands/draft.ts",
|
|
37
47
|
"packages/cli/src/commands/init.ts",
|
|
48
|
+
"packages/cli/src/commands/inspect.ts",
|
|
49
|
+
"packages/cli/src/commands/register.ts",
|
|
50
|
+
"packages/cli/src/commands/summaries.ts",
|
|
51
|
+
"packages/cli/src/commands/summarize.ts",
|
|
38
52
|
"packages/cli/src/commands/validate.ts",
|
|
39
53
|
"packages/cli/src/commands/sync.ts",
|
|
40
54
|
"packages/cli/src/commands/doctor.ts",
|
|
41
55
|
"packages/cli/src/dev/validateRepositoryContractsCli.ts",
|
|
42
56
|
"packages/cli/src/dev/verifyRuntimeSurface.ts",
|
|
43
57
|
"packages/cli/src/dev/verifyWorkflowE2E.ts",
|
|
58
|
+
"packages/cli/src/dev/verifyAgentE2E.ts",
|
|
44
59
|
"packages/cli/src/dev/verifyCleanCommand.ts",
|
|
60
|
+
"packages/adapters/src/registry.ts",
|
|
45
61
|
"packages/core/src/artifacts/registry.ts",
|
|
62
|
+
"packages/core/src/artifacts/readiness.ts",
|
|
46
63
|
"packages/core/src/contracts/index.ts",
|
|
47
64
|
"packages/core/src/contracts/yaml.ts",
|
|
48
65
|
"packages/core/src/commands/registry.ts",
|
|
49
66
|
"packages/core/src/fs/index.ts",
|
|
67
|
+
"packages/core/src/onboarding/agentsGuide.ts",
|
|
68
|
+
"packages/core/src/workflow/doctorOpenWorkflow.ts",
|
|
50
69
|
"packages/core/src/workflow/initOpenWorkflow.ts",
|
|
70
|
+
"packages/core/src/workflow/readWorkflowConfig.ts",
|
|
51
71
|
"packages/core/src/workflow/cleanOpenWorkflow.ts",
|
|
72
|
+
"packages/core/src/workflow/summaryHealth.ts",
|
|
73
|
+
"packages/core/src/workflow/syncOpenWorkflow.ts",
|
|
52
74
|
"packages/core/src/validators/validateOpenWorkflow.ts",
|
|
53
75
|
"packages/core/src/validators/validateRepositoryContracts.ts",
|
|
54
76
|
"packages/core/src/graph/README.md",
|
|
@@ -60,6 +82,7 @@ const REQUIRED_FILES = [
|
|
|
60
82
|
"packages/adapters/codex/src/templates.ts",
|
|
61
83
|
"templates/openworkflow/README.md",
|
|
62
84
|
"templates/codex/README.md",
|
|
85
|
+
"skills/build-vision/SKILL.md",
|
|
63
86
|
"skills/build-validation/SKILL.md",
|
|
64
87
|
"skills/build-validation/scripts/init_validation.py",
|
|
65
88
|
"skills/build-prototype/SKILL.md",
|
|
@@ -113,15 +136,105 @@ const REQUIRED_FILES = [
|
|
|
113
136
|
"changes/M21-npm-package-release-readiness/WORK_ITEMS.yaml",
|
|
114
137
|
"changes/M22-project-clean-command/CHANGE.yaml",
|
|
115
138
|
"changes/M22-project-clean-command/WORK_ITEMS.yaml",
|
|
139
|
+
"changes/M23-production-command-lazy-contracts/CHANGE.yaml",
|
|
140
|
+
"changes/M23-production-command-lazy-contracts/WORK_ITEMS.yaml",
|
|
141
|
+
"changes/M24-agent-context-state-and-summaries/CHANGE.yaml",
|
|
142
|
+
"changes/M24-agent-context-state-and-summaries/WORK_ITEMS.yaml",
|
|
143
|
+
"changes/M25-agent-onboarding-entrypoint/CHANGE.yaml",
|
|
144
|
+
"changes/M25-agent-onboarding-entrypoint/WORK_ITEMS.yaml",
|
|
145
|
+
"changes/M26-non-destructive-multi-platform-sync/CHANGE.yaml",
|
|
146
|
+
"changes/M26-non-destructive-multi-platform-sync/WORK_ITEMS.yaml",
|
|
147
|
+
"changes/M27-agent-brief-status/CHANGE.yaml",
|
|
148
|
+
"changes/M27-agent-brief-status/WORK_ITEMS.yaml",
|
|
149
|
+
"changes/M28-cli-json-report-surface/CHANGE.yaml",
|
|
150
|
+
"changes/M28-cli-json-report-surface/WORK_ITEMS.yaml",
|
|
151
|
+
"changes/M29-command-readiness-check/CHANGE.yaml",
|
|
152
|
+
"changes/M29-command-readiness-check/WORK_ITEMS.yaml",
|
|
153
|
+
"changes/M30-artifact-summary-health/CHANGE.yaml",
|
|
154
|
+
"changes/M30-artifact-summary-health/WORK_ITEMS.yaml",
|
|
155
|
+
"changes/M31-unified-health-semantics/CHANGE.yaml",
|
|
156
|
+
"changes/M31-unified-health-semantics/WORK_ITEMS.yaml",
|
|
157
|
+
"changes/M32-agent-inspect-entry/CHANGE.yaml",
|
|
158
|
+
"changes/M32-agent-inspect-entry/WORK_ITEMS.yaml",
|
|
159
|
+
"changes/M33-summary-refresh-command/CHANGE.yaml",
|
|
160
|
+
"changes/M33-summary-refresh-command/WORK_ITEMS.yaml",
|
|
161
|
+
"changes/M34-agent-context-packet/CHANGE.yaml",
|
|
162
|
+
"changes/M34-agent-context-packet/WORK_ITEMS.yaml",
|
|
163
|
+
"changes/M35-artifact-draft-command/CHANGE.yaml",
|
|
164
|
+
"changes/M35-artifact-draft-command/WORK_ITEMS.yaml",
|
|
165
|
+
"changes/M36-artifact-register-command/CHANGE.yaml",
|
|
166
|
+
"changes/M36-artifact-register-command/WORK_ITEMS.yaml",
|
|
167
|
+
"changes/M37-managed-clean-boundary/CHANGE.yaml",
|
|
168
|
+
"changes/M37-managed-clean-boundary/WORK_ITEMS.yaml",
|
|
169
|
+
"changes/M38-json-exit-code-semantics/CHANGE.yaml",
|
|
170
|
+
"changes/M38-json-exit-code-semantics/WORK_ITEMS.yaml",
|
|
171
|
+
"changes/M39-stage-readiness-gates/CHANGE.yaml",
|
|
172
|
+
"changes/M39-stage-readiness-gates/WORK_ITEMS.yaml",
|
|
173
|
+
"changes/M40-compact-context-default/CHANGE.yaml",
|
|
174
|
+
"changes/M40-compact-context-default/WORK_ITEMS.yaml",
|
|
175
|
+
"changes/M41-compact-command-audit-slice/CHANGE.yaml",
|
|
176
|
+
"changes/M41-compact-command-audit-slice/WORK_ITEMS.yaml",
|
|
177
|
+
"changes/M42-health-errors-surface/CHANGE.yaml",
|
|
178
|
+
"changes/M42-health-errors-surface/WORK_ITEMS.yaml",
|
|
179
|
+
"changes/M43-summary-quality-signals/CHANGE.yaml",
|
|
180
|
+
"changes/M43-summary-quality-signals/WORK_ITEMS.yaml",
|
|
181
|
+
"changes/M44-clean-sync-recovery-e2e/CHANGE.yaml",
|
|
182
|
+
"changes/M44-clean-sync-recovery-e2e/WORK_ITEMS.yaml",
|
|
183
|
+
"changes/M45-sync-state-reconciliation/CHANGE.yaml",
|
|
184
|
+
"changes/M45-sync-state-reconciliation/WORK_ITEMS.yaml",
|
|
185
|
+
"changes/M46-strict-summary-quality/CHANGE.yaml",
|
|
186
|
+
"changes/M46-strict-summary-quality/WORK_ITEMS.yaml",
|
|
187
|
+
"changes/M47-doctor-handoff-quality-split/CHANGE.yaml",
|
|
188
|
+
"changes/M47-doctor-handoff-quality-split/WORK_ITEMS.yaml",
|
|
189
|
+
"changes/M48-handoff-quality-summary/CHANGE.yaml",
|
|
190
|
+
"changes/M48-handoff-quality-summary/WORK_ITEMS.yaml",
|
|
191
|
+
"changes/M49-single-handoff-entry/CHANGE.yaml",
|
|
192
|
+
"changes/M49-single-handoff-entry/WORK_ITEMS.yaml",
|
|
193
|
+
"changes/M50-context-handoff-mode/CHANGE.yaml",
|
|
194
|
+
"changes/M50-context-handoff-mode/WORK_ITEMS.yaml",
|
|
195
|
+
"changes/M51-agent-first-e2e-suite/CHANGE.yaml",
|
|
196
|
+
"changes/M51-agent-first-e2e-suite/WORK_ITEMS.yaml",
|
|
197
|
+
"changes/M52-default-sync-adapter-recovery/CHANGE.yaml",
|
|
198
|
+
"changes/M52-default-sync-adapter-recovery/WORK_ITEMS.yaml",
|
|
116
199
|
];
|
|
117
200
|
const IGNORED_DIRS = new Set([".git", "node_modules", "dist", "build", "coverage"]);
|
|
118
201
|
const COMMON_REQUIRED = ["schema_version", "contract_id", "contract_type", "title", "status"];
|
|
202
|
+
const CODEX_SKILL_METADATA_FIELDS = [
|
|
203
|
+
"generated_by",
|
|
204
|
+
"adapter",
|
|
205
|
+
"adapter_version",
|
|
206
|
+
"template_id",
|
|
207
|
+
"source_command_id",
|
|
208
|
+
"semantic_trigger",
|
|
209
|
+
"skill_name",
|
|
210
|
+
];
|
|
211
|
+
const REQUIRED_CODEX_SKILL_BLOCKS = [
|
|
212
|
+
"user_behavior",
|
|
213
|
+
"agent_protocol",
|
|
214
|
+
"working_protocol",
|
|
215
|
+
"artifact_checkpoint",
|
|
216
|
+
"codex_skill",
|
|
217
|
+
];
|
|
218
|
+
const HIGH_RISK_REPORT_SECTIONS = [
|
|
219
|
+
"Trigger",
|
|
220
|
+
"Change",
|
|
221
|
+
"Concrete Risks",
|
|
222
|
+
"Decision Options",
|
|
223
|
+
"Recommended Path",
|
|
224
|
+
"Guardrails",
|
|
225
|
+
"Go Criteria",
|
|
226
|
+
"Stop Criteria",
|
|
227
|
+
"Validation Expectations",
|
|
228
|
+
];
|
|
119
229
|
export async function validateRepositoryContracts(rootInput) {
|
|
120
230
|
const root = resolve(rootInput);
|
|
121
231
|
const errors = [];
|
|
122
232
|
await validateRequiredFiles(root, errors);
|
|
123
233
|
await validateJsonSchemas(root, errors);
|
|
124
234
|
await validateYamlContracts(root, errors);
|
|
235
|
+
await validateGeneratedCodexSkills(root, errors);
|
|
236
|
+
await validateGeneratedSurfaceParity(root, errors);
|
|
237
|
+
await validateHighRiskDecisionReports(root, errors);
|
|
125
238
|
return { ok: errors.length === 0, errors };
|
|
126
239
|
}
|
|
127
240
|
async function validateRequiredFiles(root, errors) {
|
|
@@ -171,417 +284,2267 @@ async function validateYamlContracts(root, errors) {
|
|
|
171
284
|
validatePrototype(root, path, data, errors);
|
|
172
285
|
validateArtifactContracts(root, path, data, errors);
|
|
173
286
|
validateDisclosureLevels(root, path, data, errors);
|
|
287
|
+
validateConfig(root, path, data, errors);
|
|
288
|
+
validateCurrentState(root, path, data, errors);
|
|
174
289
|
validateActivePointer(root, path, data, errors);
|
|
175
290
|
validateDiscoveryArtifact(root, path, data, errors);
|
|
176
291
|
validateWorkflowIndex(root, path, data, errors);
|
|
177
292
|
validateContractGraph(root, path, data, errors);
|
|
293
|
+
await validateCandidateChanges(root, path, data, errors);
|
|
294
|
+
validateLocalCommitEvidence(root, path, data, errors);
|
|
178
295
|
}
|
|
179
296
|
}
|
|
180
297
|
}
|
|
181
|
-
function
|
|
182
|
-
|
|
298
|
+
async function validateGeneratedCodexSkills(root, errors) {
|
|
299
|
+
const manifestPath = join(root, ".agents", "openworkflow-adapter.yaml");
|
|
300
|
+
if (!(await exists(manifestPath))) {
|
|
183
301
|
return;
|
|
184
302
|
}
|
|
185
|
-
|
|
303
|
+
const label = relative(root, manifestPath);
|
|
304
|
+
let manifest;
|
|
305
|
+
try {
|
|
306
|
+
manifest = parseYaml(await readFile(manifestPath, "utf8"));
|
|
307
|
+
}
|
|
308
|
+
catch (error) {
|
|
309
|
+
errors.push(`${label} is not valid YAML for generated skill validation: ${messageFor(error)}`);
|
|
186
310
|
return;
|
|
187
311
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
312
|
+
if (!isRecord(manifest)) {
|
|
313
|
+
errors.push(`${label} must be a mapping`);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
if (manifest.generated_by !== "openworkflow") {
|
|
317
|
+
errors.push(`${label} generated_by must be openworkflow for Codex skill validation`);
|
|
318
|
+
}
|
|
319
|
+
if (manifest.adapter !== "codex") {
|
|
320
|
+
errors.push(`${label} adapter must be codex for Codex skill validation`);
|
|
321
|
+
}
|
|
322
|
+
const adapterVersion = typeof manifest.adapter_version === "string" ? manifest.adapter_version : null;
|
|
323
|
+
if (!adapterVersion) {
|
|
324
|
+
errors.push(`${label} adapter_version must be a non-empty string`);
|
|
325
|
+
}
|
|
326
|
+
const namespace = typeof manifest.command_namespace === "string" ? manifest.command_namespace : "ow";
|
|
327
|
+
validateManifestSkillSurface(label, manifest.skill_surface, errors);
|
|
328
|
+
const commands = manifest.commands;
|
|
329
|
+
if (!Array.isArray(commands) || commands.length === 0) {
|
|
330
|
+
errors.push(`${label} commands must be a non-empty list`);
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
for (const [index, command] of commands.entries()) {
|
|
334
|
+
if (!isRecord(command)) {
|
|
335
|
+
errors.push(`${label} command ${index} must be a mapping`);
|
|
336
|
+
continue;
|
|
192
337
|
}
|
|
338
|
+
await validateGeneratedCodexSkill(root, label, command, namespace, adapterVersion ?? "", errors);
|
|
193
339
|
}
|
|
194
|
-
|
|
195
|
-
|
|
340
|
+
}
|
|
341
|
+
function validateManifestSkillSurface(label, value, errors) {
|
|
342
|
+
if (!isRecord(value)) {
|
|
343
|
+
errors.push(`${label} skill_surface must be a mapping`);
|
|
344
|
+
return;
|
|
196
345
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
346
|
+
const frontmatter = value.frontmatter;
|
|
347
|
+
if (!Array.isArray(frontmatter) || !["name", "description", "metadata"].every((field) => frontmatter.includes(field))) {
|
|
348
|
+
errors.push(`${label} skill_surface.frontmatter must include name, description, and metadata`);
|
|
349
|
+
}
|
|
350
|
+
const metadataFields = value.metadata_fields;
|
|
351
|
+
if (!Array.isArray(metadataFields)) {
|
|
352
|
+
errors.push(`${label} skill_surface.metadata_fields must list generated metadata fields`);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
for (const field of CODEX_SKILL_METADATA_FIELDS) {
|
|
356
|
+
if (!metadataFields.includes(field)) {
|
|
357
|
+
errors.push(`${label} skill_surface.metadata_fields missing ${field}`);
|
|
205
358
|
}
|
|
206
359
|
}
|
|
207
360
|
}
|
|
208
|
-
function
|
|
209
|
-
|
|
361
|
+
async function validateGeneratedCodexSkill(root, manifestLabel, command, namespace, adapterVersion, errors) {
|
|
362
|
+
const commandId = stringField(command, "id");
|
|
363
|
+
const trigger = stringField(command, "trigger");
|
|
364
|
+
const skillName = stringField(command, "skill_name");
|
|
365
|
+
const skillPath = stringField(command, "skill_path");
|
|
366
|
+
const commandLabel = `${manifestLabel} command ${commandId || "<unknown>"}`;
|
|
367
|
+
if (!commandId || !trigger || !skillName || !skillPath) {
|
|
368
|
+
errors.push(`${commandLabel} must include id, trigger, skill_name, and skill_path`);
|
|
210
369
|
return;
|
|
211
370
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
371
|
+
const absoluteSkillPath = join(root, skillPath);
|
|
372
|
+
if (!existsSyncSafe(absoluteSkillPath)) {
|
|
373
|
+
errors.push(`${commandLabel} references missing generated skill ${skillPath}; update adapter source and run openworkflow sync --tools codex`);
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
let content;
|
|
377
|
+
try {
|
|
378
|
+
if (!statSync(absoluteSkillPath).isFile()) {
|
|
379
|
+
errors.push(`${skillPath} must be a generated skill file`);
|
|
380
|
+
return;
|
|
215
381
|
}
|
|
382
|
+
content = await readFile(absoluteSkillPath, "utf8");
|
|
216
383
|
}
|
|
217
|
-
|
|
218
|
-
errors.push(`${
|
|
384
|
+
catch (error) {
|
|
385
|
+
errors.push(`${skillPath} could not be read for generated skill validation: ${messageFor(error)}`);
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
const frontmatter = parseSkillFrontmatter(skillPath, content, errors);
|
|
389
|
+
if (!frontmatter) {
|
|
390
|
+
return;
|
|
219
391
|
}
|
|
392
|
+
validateSkillFrontmatter(skillPath, frontmatter, {
|
|
393
|
+
adapterVersion,
|
|
394
|
+
commandId,
|
|
395
|
+
namespace,
|
|
396
|
+
skillName,
|
|
397
|
+
trigger,
|
|
398
|
+
}, errors);
|
|
399
|
+
validateSkillGeneratedMarker(skillPath, content, namespace, commandId, errors);
|
|
400
|
+
validateSkillProtocolBlocks(skillPath, content, errors);
|
|
220
401
|
}
|
|
221
|
-
function
|
|
222
|
-
if (
|
|
402
|
+
function parseSkillFrontmatter(skillPath, content, errors) {
|
|
403
|
+
if (!content.startsWith("---\n")) {
|
|
404
|
+
errors.push(`${skillPath} missing SKILL.md frontmatter; update adapter source and run openworkflow sync --tools codex`);
|
|
405
|
+
return null;
|
|
406
|
+
}
|
|
407
|
+
const frontmatterEnd = content.indexOf("\n---\n", 4);
|
|
408
|
+
if (frontmatterEnd === -1) {
|
|
409
|
+
errors.push(`${skillPath} has unterminated SKILL.md frontmatter`);
|
|
410
|
+
return null;
|
|
411
|
+
}
|
|
412
|
+
let frontmatter;
|
|
413
|
+
try {
|
|
414
|
+
frontmatter = parseYaml(content.slice(4, frontmatterEnd));
|
|
415
|
+
}
|
|
416
|
+
catch (error) {
|
|
417
|
+
errors.push(`${skillPath} has invalid SKILL.md frontmatter YAML: ${messageFor(error)}`);
|
|
418
|
+
return null;
|
|
419
|
+
}
|
|
420
|
+
if (!isRecord(frontmatter)) {
|
|
421
|
+
errors.push(`${skillPath} SKILL.md frontmatter must be a mapping`);
|
|
422
|
+
return null;
|
|
423
|
+
}
|
|
424
|
+
return frontmatter;
|
|
425
|
+
}
|
|
426
|
+
function validateSkillFrontmatter(skillPath, frontmatter, expected, errors) {
|
|
427
|
+
if (frontmatter.name !== expected.skillName) {
|
|
428
|
+
errors.push(`${skillPath} frontmatter.name must be ${expected.skillName}`);
|
|
429
|
+
}
|
|
430
|
+
if (!nonEmptyString(frontmatter.description)) {
|
|
431
|
+
errors.push(`${skillPath} frontmatter.description must be a non-empty string`);
|
|
432
|
+
}
|
|
433
|
+
const metadata = frontmatter.metadata;
|
|
434
|
+
if (!isRecord(metadata)) {
|
|
435
|
+
errors.push(`${skillPath} missing generated metadata; update adapter source and run openworkflow sync --tools codex`);
|
|
223
436
|
return;
|
|
224
437
|
}
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
438
|
+
const expectedMetadata = {
|
|
439
|
+
generated_by: "openworkflow",
|
|
440
|
+
adapter: "codex",
|
|
441
|
+
adapter_version: expected.adapterVersion,
|
|
442
|
+
template_id: `codex.skill.${expected.namespace}.${expected.commandId}`,
|
|
443
|
+
source_command_id: expected.commandId,
|
|
444
|
+
semantic_trigger: expected.trigger,
|
|
445
|
+
skill_name: expected.skillName,
|
|
446
|
+
};
|
|
447
|
+
for (const field of CODEX_SKILL_METADATA_FIELDS) {
|
|
448
|
+
if (metadata[field] !== expectedMetadata[field]) {
|
|
449
|
+
errors.push(`${skillPath} metadata.${field} must be ${expectedMetadata[field]}`);
|
|
450
|
+
}
|
|
228
451
|
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
452
|
+
}
|
|
453
|
+
function validateSkillGeneratedMarker(skillPath, content, namespace, commandId, errors) {
|
|
454
|
+
if (!content.includes("generated-by: openworkflow")) {
|
|
455
|
+
errors.push(`${skillPath} missing generated marker; update adapter source and run openworkflow sync --tools codex`);
|
|
232
456
|
}
|
|
233
|
-
|
|
234
|
-
|
|
457
|
+
const templateMarker = `template-id: codex.skill.${namespace}.${commandId}`;
|
|
458
|
+
if (!content.includes(templateMarker)) {
|
|
459
|
+
errors.push(`${skillPath} missing generated marker ${templateMarker}`);
|
|
235
460
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
461
|
+
}
|
|
462
|
+
function validateSkillProtocolBlocks(skillPath, content, errors) {
|
|
463
|
+
if (/<\/?skill(?:\s|>)/.test(content)) {
|
|
464
|
+
errors.push(`${skillPath} must not use a top-level <skill> XML wrapper; generated skills are Markdown with YAML frontmatter and XML-like protocol blocks`);
|
|
465
|
+
}
|
|
466
|
+
for (const tag of REQUIRED_CODEX_SKILL_BLOCKS) {
|
|
467
|
+
validateRequiredSkillBlock(skillPath, content, tag, errors);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
function validateRequiredSkillBlock(skillPath, content, tag, errors) {
|
|
471
|
+
const openTag = `<${tag}>`;
|
|
472
|
+
const closeTag = `</${tag}>`;
|
|
473
|
+
const openCount = countOccurrences(content, openTag);
|
|
474
|
+
const closeCount = countOccurrences(content, closeTag);
|
|
475
|
+
if (openCount === 0 || closeCount === 0 || openCount !== closeCount) {
|
|
476
|
+
errors.push(`${skillPath} must contain a balanced <${tag}> protocol block`);
|
|
239
477
|
return;
|
|
240
478
|
}
|
|
241
|
-
|
|
242
|
-
|
|
479
|
+
if (tag !== "artifact_checkpoint" && (openCount !== 1 || closeCount !== 1)) {
|
|
480
|
+
errors.push(`${skillPath} must contain exactly one <${tag}> protocol block`);
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
if (content.indexOf(openTag) > content.indexOf(closeTag)) {
|
|
484
|
+
errors.push(`${skillPath} <${tag}> protocol block closes before it opens`);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
function countOccurrences(content, needle) {
|
|
488
|
+
return content.split(needle).length - 1;
|
|
489
|
+
}
|
|
490
|
+
function stringField(record, key) {
|
|
491
|
+
const value = record[key];
|
|
492
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
493
|
+
}
|
|
494
|
+
function recordField(record, key) {
|
|
495
|
+
const value = record[key];
|
|
496
|
+
return isRecord(value) ? value : {};
|
|
497
|
+
}
|
|
498
|
+
async function validateGeneratedSurfaceParity(root, errors) {
|
|
499
|
+
const commands = [...getWorkflowCommands()];
|
|
500
|
+
const commandAudit = await readYamlRecordForParity(root, ".openworkflow/audit/COMMAND_AUDIT_INDEX.yaml", errors);
|
|
501
|
+
if (commandAudit) {
|
|
502
|
+
validateCommandAuditParity(".openworkflow/audit/COMMAND_AUDIT_INDEX.yaml", commandAudit.commands, commands, errors);
|
|
503
|
+
}
|
|
504
|
+
const contextPackets = await readYamlRecordForParity(root, ".openworkflow/audit/CONTEXT_PACKETS.yaml", errors);
|
|
505
|
+
if (contextPackets) {
|
|
506
|
+
validateContextPacketParity(".openworkflow/audit/CONTEXT_PACKETS.yaml", contextPackets.packets, commands, errors);
|
|
507
|
+
}
|
|
508
|
+
const codexManifest = await readYamlRecordForParity(root, ".agents/openworkflow-adapter.yaml", errors);
|
|
509
|
+
if (codexManifest) {
|
|
510
|
+
await validateCodexManifestParity(root, ".agents/openworkflow-adapter.yaml", codexManifest, commands, errors);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
async function readYamlRecordForParity(root, relativePath, errors) {
|
|
514
|
+
const path = join(root, relativePath);
|
|
515
|
+
if (!(await exists(path))) {
|
|
516
|
+
return null;
|
|
517
|
+
}
|
|
518
|
+
try {
|
|
519
|
+
const data = parseYaml(await readFile(path, "utf8"));
|
|
520
|
+
if (isRecord(data)) {
|
|
521
|
+
return data;
|
|
522
|
+
}
|
|
523
|
+
errors.push(`${relativePath} must be a mapping for generated-surface parity validation`);
|
|
524
|
+
}
|
|
525
|
+
catch (error) {
|
|
526
|
+
errors.push(`${relativePath} is not valid YAML for generated-surface parity validation: ${messageFor(error)}`);
|
|
527
|
+
}
|
|
528
|
+
return null;
|
|
529
|
+
}
|
|
530
|
+
function validateCommandAuditParity(label, value, commands, errors) {
|
|
531
|
+
const records = recordsById(label, "commands", value, "id", errors);
|
|
532
|
+
if (!records) {
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
assertCommandIds(label, records, commands, errors);
|
|
536
|
+
for (const command of commands) {
|
|
537
|
+
const actual = records.get(command.id);
|
|
538
|
+
if (!actual) {
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
assertField(label, command.id, actual, "trigger", command.trigger, errors);
|
|
542
|
+
assertField(label, command.id, actual, "stage", command.stage, errors);
|
|
543
|
+
assertField(label, command.id, actual, "visibility", command.visibility, errors);
|
|
544
|
+
assertField(label, command.id, actual, "depth", command.protocol?.depth ?? "shallow", errors);
|
|
545
|
+
assertField(label, command.id, actual, "context_packet", `context:${command.id}`, errors);
|
|
546
|
+
assertStringArray(label, command.id, actual, "allowed_outputs", command.protocol?.allowedOutputs ?? command.targetArtifacts, errors);
|
|
547
|
+
assertStringArray(label, command.id, actual, "conditional_outputs", command.protocol?.conditionalOutputs ?? [], errors);
|
|
548
|
+
assertStringArray(label, command.id, actual, "forbidden_outputs", command.protocol?.forbiddenOutputs ?? [], errors);
|
|
549
|
+
assertStringArray(label, command.id, actual, "handoff_commands", command.protocol?.handoffCommands ?? [], errors);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
function validateContextPacketParity(label, value, commands, errors) {
|
|
553
|
+
const records = recordsById(label, "packets", value, "packet_id", errors);
|
|
554
|
+
if (!records) {
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
const expectedIds = new Set(commands.map((command) => `context:${command.id}`));
|
|
558
|
+
assertIdSet(label, "packets", records, expectedIds, errors);
|
|
559
|
+
for (const command of commands) {
|
|
560
|
+
const packetId = `context:${command.id}`;
|
|
561
|
+
const actual = records.get(packetId);
|
|
562
|
+
if (!actual) {
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
assertField(label, packetId, actual, "command", command.trigger, errors);
|
|
566
|
+
assertField(label, packetId, actual, "visibility", command.visibility, errors);
|
|
567
|
+
assertStringArray(label, packetId, actual, "required", command.protocol?.requiredContext ?? [".openworkflow/workflow/WORKFLOW_INDEX.yaml"], errors);
|
|
568
|
+
assertStringArray(label, packetId, actual, "optional", command.protocol?.optionalContext ?? [], errors);
|
|
569
|
+
assertStringArray(label, packetId, actual, "forbidden", command.protocol?.forbiddenContext ?? [], errors);
|
|
570
|
+
assertStringArray(label, packetId, actual, "conditional_outputs", command.protocol?.conditionalOutputs ?? [], errors);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
async function validateCodexManifestParity(root, label, manifest, commands, errors) {
|
|
574
|
+
const records = recordsById(label, "commands", manifest.commands, "id", errors);
|
|
575
|
+
if (!records) {
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
assertCommandIds(label, records, commands, errors);
|
|
579
|
+
const expectedGeneratedFiles = new Set();
|
|
580
|
+
for (const command of commands) {
|
|
581
|
+
const actual = records.get(command.id);
|
|
582
|
+
if (!actual) {
|
|
583
|
+
continue;
|
|
584
|
+
}
|
|
585
|
+
const skillName = `ow-${command.id}`;
|
|
586
|
+
const skillPath = `.agents/skills/${skillName}/SKILL.md`;
|
|
587
|
+
const interfacePath = `.agents/skills/${skillName}/agents/openai.yaml`;
|
|
588
|
+
expectedGeneratedFiles.add(skillPath);
|
|
589
|
+
expectedGeneratedFiles.add(interfacePath);
|
|
590
|
+
assertField(label, command.id, actual, "trigger", command.trigger, errors);
|
|
591
|
+
assertField(label, command.id, actual, "visibility", command.visibility, errors);
|
|
592
|
+
assertField(label, command.id, actual, "skill_name", skillName, errors);
|
|
593
|
+
assertField(label, command.id, actual, "explicit_invocation", `$${skillName}`, errors);
|
|
594
|
+
assertField(label, command.id, actual, "skill_path", skillPath, errors);
|
|
595
|
+
assertField(label, command.id, actual, "interface_path", interfacePath, errors);
|
|
596
|
+
assertStringArray(label, command.id, actual, "legacy_triggers", command.legacyTriggers, errors);
|
|
597
|
+
}
|
|
598
|
+
await validateManifestGeneratedFiles(root, label, manifest.generated_files, expectedGeneratedFiles, errors);
|
|
599
|
+
}
|
|
600
|
+
async function validateManifestGeneratedFiles(root, label, value, expectedGeneratedFiles, errors) {
|
|
601
|
+
if (!Array.isArray(value)) {
|
|
602
|
+
errors.push(`${label} generated_files must be a list; update adapter source and run openworkflow sync --tools codex`);
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
const actual = new Set();
|
|
606
|
+
for (const item of value) {
|
|
607
|
+
if (typeof item !== "string") {
|
|
608
|
+
errors.push(`${label} generated_files contains a non-string path`);
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
actual.add(item);
|
|
612
|
+
const path = join(root, item);
|
|
613
|
+
if (!existsSyncSafe(path)) {
|
|
614
|
+
errors.push(`${label} generated_files references missing ${item}; update adapter source and run openworkflow sync --tools codex`);
|
|
615
|
+
continue;
|
|
616
|
+
}
|
|
617
|
+
const content = await readFile(path, "utf8");
|
|
618
|
+
if (!content.includes("generated-by: openworkflow")) {
|
|
619
|
+
errors.push(`${label} generated_files ${item} is missing generated marker; update adapter source and run openworkflow sync --tools codex`);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
for (const expected of expectedGeneratedFiles) {
|
|
623
|
+
if (!actual.has(expected)) {
|
|
624
|
+
errors.push(`${label} generated_files missing ${expected}; update adapter source and run openworkflow sync --tools codex`);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
function recordsById(label, collectionName, value, idKey, errors) {
|
|
629
|
+
if (!Array.isArray(value)) {
|
|
630
|
+
errors.push(`${label} ${collectionName} must be a list for generated-surface parity validation`);
|
|
631
|
+
return null;
|
|
632
|
+
}
|
|
633
|
+
const records = new Map();
|
|
634
|
+
value.forEach((item, index) => {
|
|
243
635
|
if (!isRecord(item)) {
|
|
244
|
-
errors.push(`${label}
|
|
636
|
+
errors.push(`${label} ${collectionName}[${index}] must be a mapping`);
|
|
245
637
|
return;
|
|
246
638
|
}
|
|
247
|
-
const
|
|
248
|
-
if (typeof
|
|
249
|
-
errors.push(`${label}
|
|
639
|
+
const id = item[idKey];
|
|
640
|
+
if (typeof id !== "string" || id.length === 0) {
|
|
641
|
+
errors.push(`${label} ${collectionName}[${index}] missing ${idKey}`);
|
|
250
642
|
return;
|
|
251
643
|
}
|
|
252
|
-
if (
|
|
253
|
-
errors.push(`${label} duplicate
|
|
254
|
-
|
|
255
|
-
seen.add(taskId);
|
|
256
|
-
for (const key of ["title", "status", "owned_paths", "acceptance"]) {
|
|
257
|
-
if (!(key in item)) {
|
|
258
|
-
errors.push(`${label} ${taskId} missing ${key}`);
|
|
259
|
-
}
|
|
644
|
+
if (records.has(id)) {
|
|
645
|
+
errors.push(`${label} ${collectionName} duplicate ${idKey} ${id}`);
|
|
646
|
+
return;
|
|
260
647
|
}
|
|
648
|
+
records.set(id, item);
|
|
261
649
|
});
|
|
650
|
+
return records;
|
|
262
651
|
}
|
|
263
|
-
function
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
652
|
+
function assertCommandIds(label, records, commands, errors) {
|
|
653
|
+
assertIdSet(label, "commands", records, new Set(commands.map((command) => command.id)), errors);
|
|
654
|
+
}
|
|
655
|
+
function assertIdSet(label, collectionName, records, expectedIds, errors) {
|
|
656
|
+
for (const expected of expectedIds) {
|
|
657
|
+
if (!records.has(expected)) {
|
|
658
|
+
errors.push(`${label} ${collectionName} missing ${expected}; update source registry and run openworkflow sync --tools codex`);
|
|
659
|
+
}
|
|
270
660
|
}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
"feature_classification",
|
|
275
|
-
"critical_assumptions",
|
|
276
|
-
"prototype_scope",
|
|
277
|
-
"acceptance",
|
|
278
|
-
"decision_options",
|
|
279
|
-
]) {
|
|
280
|
-
if (!(key in data)) {
|
|
281
|
-
errors.push(`${label} missing validation key ${key}`);
|
|
661
|
+
for (const actual of records.keys()) {
|
|
662
|
+
if (!expectedIds.has(actual)) {
|
|
663
|
+
errors.push(`${label} ${collectionName} has unexpected ${actual}; update source registry and run openworkflow sync --tools codex`);
|
|
282
664
|
}
|
|
283
665
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
666
|
+
}
|
|
667
|
+
function assertField(label, id, record, field, expected, errors) {
|
|
668
|
+
if (record[field] !== expected) {
|
|
669
|
+
errors.push(`${label} ${id} ${field} must be ${expected}; update source registry and run openworkflow sync --tools codex`);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
function assertStringArray(label, id, record, field, expected, errors) {
|
|
673
|
+
const actual = record[field];
|
|
674
|
+
if (!Array.isArray(actual) || !actual.every((item) => typeof item === "string")) {
|
|
675
|
+
errors.push(`${label} ${id} ${field} must be a string list; update source registry and run openworkflow sync --tools codex`);
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
if (!stringArraysEqual(actual, expected)) {
|
|
679
|
+
errors.push(`${label} ${id} ${field} drifted from source registry; update source registry and run openworkflow sync --tools codex`);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
function stringArraysEqual(actual, expected) {
|
|
683
|
+
return actual.length === expected.length && actual.every((item, index) => item === expected[index]);
|
|
684
|
+
}
|
|
685
|
+
async function validateHighRiskDecisionReports(root, errors) {
|
|
686
|
+
for (const path of await findFiles(root, (entry) => entry === "HIGH_RISK_DECISION_REPORT.md")) {
|
|
687
|
+
const label = relative(root, path);
|
|
688
|
+
const content = await readFile(path, "utf8");
|
|
689
|
+
for (const section of HIGH_RISK_REPORT_SECTIONS) {
|
|
690
|
+
if (!hasMarkdownHeading(content, section)) {
|
|
691
|
+
errors.push(`${label} missing high-risk report section: ${section}`);
|
|
290
692
|
}
|
|
291
693
|
}
|
|
694
|
+
if (!content.includes("explicit") || !content.includes("approval")) {
|
|
695
|
+
errors.push(`${label} must state that implementation resumes only after explicit approval`);
|
|
696
|
+
}
|
|
292
697
|
}
|
|
293
698
|
}
|
|
294
|
-
function
|
|
295
|
-
|
|
699
|
+
function hasMarkdownHeading(content, heading) {
|
|
700
|
+
const escaped = heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
701
|
+
return new RegExp(`^#{2,4}\\s+.*${escaped}.*$`, "m").test(content);
|
|
702
|
+
}
|
|
703
|
+
function validateConfig(root, path, data, errors) {
|
|
704
|
+
if (basename(path) !== "config.yaml") {
|
|
296
705
|
return;
|
|
297
706
|
}
|
|
298
707
|
const label = relative(root, path);
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
708
|
+
if (!nonEmptyString(data.project_slug) || data.project_slug === "project") {
|
|
709
|
+
errors.push(`${label} project_slug must be a useful non-empty slug`);
|
|
710
|
+
}
|
|
711
|
+
if (!nonEmptyString(data.project_title) || data.project_title === ".") {
|
|
712
|
+
errors.push(`${label} project_title must be a useful non-empty title`);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
function validateCurrentState(root, path, data, errors) {
|
|
716
|
+
if (basename(path) !== "CURRENT_STATE.yaml") {
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
const label = relative(root, path);
|
|
720
|
+
for (const key of ["active_stage", "next_command", "blocked_by", "read_this_first", "last_decision"]) {
|
|
308
721
|
if (!(key in data)) {
|
|
309
|
-
errors.push(`${label} missing
|
|
722
|
+
errors.push(`${label} missing current state key ${key}`);
|
|
310
723
|
}
|
|
311
724
|
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
const artifact = data.artifact;
|
|
315
|
-
if (isRecord(artifact) && typeof artifact.path === "string" && !existsSyncSafe(join(contractRootFor(root, path), artifact.path))) {
|
|
316
|
-
errors.push(`${label} references missing artifact path ${artifact.path}`);
|
|
725
|
+
if (!nonEmptyString(data.active_stage)) {
|
|
726
|
+
errors.push(`${label} active_stage must be a non-empty string`);
|
|
317
727
|
}
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
728
|
+
if (typeof data.next_command !== "string" && data.next_command !== null) {
|
|
729
|
+
errors.push(`${label} next_command must be a string or null`);
|
|
730
|
+
}
|
|
731
|
+
for (const key of ["blocked_by", "read_this_first"]) {
|
|
732
|
+
if (!Array.isArray(data[key])) {
|
|
733
|
+
errors.push(`${label} ${key} must be an array`);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
if (!isRecord(data.last_decision)) {
|
|
737
|
+
errors.push(`${label} last_decision must be a mapping`);
|
|
321
738
|
}
|
|
322
739
|
}
|
|
323
|
-
function
|
|
324
|
-
if (basename(path) !== "
|
|
740
|
+
async function validateCandidateChanges(root, path, data, errors) {
|
|
741
|
+
if (basename(path) !== "CANDIDATE_CHANGES.yaml") {
|
|
325
742
|
return;
|
|
326
743
|
}
|
|
327
744
|
const label = relative(root, path);
|
|
328
|
-
|
|
329
|
-
if (!Array.isArray(artifacts) || artifacts.length === 0) {
|
|
330
|
-
errors.push(`${label} must contain artifacts`);
|
|
745
|
+
if (data.planning_artifact_type !== "candidate_changes") {
|
|
331
746
|
return;
|
|
332
747
|
}
|
|
333
|
-
const
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
errors.push(`${label} artifact ${index} missing ${key}`);
|
|
354
|
-
}
|
|
748
|
+
const queuePolicy = data.queue_policy;
|
|
749
|
+
if (isRecord(queuePolicy) && "branch_boundary" in queuePolicy) {
|
|
750
|
+
validateBranchBoundary(label, queuePolicy.branch_boundary, errors);
|
|
751
|
+
}
|
|
752
|
+
if (isRecord(queuePolicy) && "branch_identity_exception" in queuePolicy) {
|
|
753
|
+
validateBranchIdentityException(label, queuePolicy.branch_identity_exception, errors);
|
|
754
|
+
}
|
|
755
|
+
const strictCommitGate = isRecord(queuePolicy) && queuePolicy.selected_change_commit_gate === "strict";
|
|
756
|
+
const strictLifecycleGate = isRecord(queuePolicy) && queuePolicy.git_lifecycle_gate === "strict";
|
|
757
|
+
if (strictLifecycleGate) {
|
|
758
|
+
validateStrictGitLifecycle(root, label, data, queuePolicy, errors);
|
|
759
|
+
}
|
|
760
|
+
if (!Array.isArray(data.changes)) {
|
|
761
|
+
errors.push(`${label} changes must be a list`);
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
for (const candidate of data.changes) {
|
|
765
|
+
if (!isRecord(candidate)) {
|
|
766
|
+
errors.push(`${label} changes entries must be mappings`);
|
|
767
|
+
continue;
|
|
355
768
|
}
|
|
356
|
-
|
|
357
|
-
});
|
|
358
|
-
for (const artifactType of [...missing].sort()) {
|
|
359
|
-
errors.push(`${label} missing artifact_type ${artifactType}`);
|
|
769
|
+
validateCandidateCompletionEvidence(root, label, candidate, errors, { strictCommitGate });
|
|
360
770
|
}
|
|
361
771
|
}
|
|
362
|
-
function
|
|
363
|
-
const
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
772
|
+
function validateStrictGitLifecycle(root, label, data, queuePolicy, errors) {
|
|
773
|
+
const planId = stringField(data, "plan_id");
|
|
774
|
+
const branchBoundary = stringField(queuePolicy, "branch_boundary");
|
|
775
|
+
if (!branchBoundary) {
|
|
776
|
+
errors.push(`${label} strict git lifecycle requires queue_policy.branch_boundary`);
|
|
777
|
+
}
|
|
778
|
+
else if (planId) {
|
|
779
|
+
const branchIdentity = assessBranchIdentity(planId, branchBoundary, branchIdentityExceptionFrom(queuePolicy.branch_identity_exception), "validate");
|
|
780
|
+
errors.push(...branchIdentity.errors.map((error) => `${label} ${error}`));
|
|
781
|
+
}
|
|
782
|
+
const status = stringField(data, "status");
|
|
783
|
+
if (status === "completed" || status === "done") {
|
|
784
|
+
validateCompletedQueuePrEvidence(root, label, data, errors);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
function validateCompletedQueuePrEvidence(root, label, data, errors) {
|
|
788
|
+
const evidencePath = queuePrEvidencePath(data);
|
|
789
|
+
if (!evidencePath) {
|
|
790
|
+
errors.push(`${label} strict git lifecycle completed queue must include DRAFT_PR_OPERATION_EVIDENCE.yaml`);
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
if (isExternalRef(evidencePath) || evidencePath.startsWith("commit:")) {
|
|
794
|
+
errors.push(`${label} DRAFT_PR_OPERATION_EVIDENCE.yaml path must be repo-relative`);
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
const resolved = resolve(root, evidencePath);
|
|
798
|
+
if (resolved !== root && !resolved.startsWith(`${root}/`)) {
|
|
799
|
+
errors.push(`${label} DRAFT_PR_OPERATION_EVIDENCE.yaml path escapes repository root`);
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
if (!existsSyncSafe(resolved)) {
|
|
803
|
+
errors.push(`${label} references missing DRAFT_PR_OPERATION_EVIDENCE.yaml ${evidencePath}`);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
function queuePrEvidencePath(data) {
|
|
807
|
+
const completion = recordField(data, "completion");
|
|
808
|
+
const explicit = stringField(completion, "draft_pr_evidence_path")
|
|
809
|
+
?? stringField(completion, "draft_pr_evidence")
|
|
810
|
+
?? stringField(completion, "pr_evidence_path")
|
|
811
|
+
?? stringField(completion, "pr_evidence");
|
|
812
|
+
if (explicit && explicit.endsWith("DRAFT_PR_OPERATION_EVIDENCE.yaml")) {
|
|
813
|
+
return explicit;
|
|
814
|
+
}
|
|
815
|
+
const evidence = Array.isArray(completion.evidence) ? completion.evidence : [];
|
|
816
|
+
for (const item of evidence) {
|
|
817
|
+
if (typeof item === "string" && item.endsWith("DRAFT_PR_OPERATION_EVIDENCE.yaml")) {
|
|
818
|
+
return item;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
return null;
|
|
822
|
+
}
|
|
823
|
+
function validateBranchIdentityException(label, value, errors) {
|
|
824
|
+
if (!isRecord(value)) {
|
|
825
|
+
errors.push(`${label} queue_policy.branch_identity_exception must be a mapping when present`);
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
if (value.mode !== "temporary_continuation_branch") {
|
|
829
|
+
errors.push(`${label} queue_policy.branch_identity_exception.mode must be temporary_continuation_branch`);
|
|
830
|
+
}
|
|
831
|
+
if (value.approved !== true) {
|
|
832
|
+
errors.push(`${label} queue_policy.branch_identity_exception.approved must be true`);
|
|
833
|
+
}
|
|
834
|
+
if (!Array.isArray(value.allowed_operations) || !value.allowed_operations.every((item) => typeof item === "string")) {
|
|
835
|
+
errors.push(`${label} queue_policy.branch_identity_exception.allowed_operations must be a string list`);
|
|
836
|
+
}
|
|
837
|
+
if (!nonEmptyString(value.reason)) {
|
|
838
|
+
errors.push(`${label} queue_policy.branch_identity_exception.reason must explain the temporary continuation branch`);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
function validateBranchBoundary(label, value, errors) {
|
|
842
|
+
if (!nonEmptyString(value)) {
|
|
843
|
+
errors.push(`${label} queue_policy.branch_boundary must be a non-empty string when present`);
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
const branch = String(value).trim();
|
|
847
|
+
if (branch !== value || branch.includes(" ") || branch.startsWith("/") || branch.endsWith("/")) {
|
|
848
|
+
errors.push(`${label} queue_policy.branch_boundary must be a branch-like string without spaces or leading/trailing slashes`);
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
function validateCandidateCompletionEvidence(root, label, candidate, errors, options) {
|
|
852
|
+
if (candidate.status !== "done") {
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
const id = typeof candidate.id === "string" ? candidate.id : "<unknown>";
|
|
856
|
+
const completion = candidate.completion;
|
|
857
|
+
if (!isRecord(completion)) {
|
|
858
|
+
errors.push(`${label} ${id} done candidate must include completion evidence`);
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
const evidence = completion.evidence;
|
|
862
|
+
if (!Array.isArray(evidence) || evidence.length === 0) {
|
|
863
|
+
errors.push(`${label} ${id} completion.evidence must be a non-empty list`);
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
for (const item of evidence) {
|
|
867
|
+
if (typeof item !== "string") {
|
|
868
|
+
errors.push(`${label} ${id} completion.evidence values must be strings`);
|
|
869
|
+
continue;
|
|
870
|
+
}
|
|
871
|
+
if (item.startsWith("commit:") && !/^commit:\s+[0-9a-f]{7,40}$/i.test(item)) {
|
|
872
|
+
errors.push(`${label} ${id} completion commit evidence must use 'commit: <7-40 hex chars>'`);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
const selectedChangeId = stringField(recordField(candidate, "selection"), "selected_change_id");
|
|
876
|
+
const implementationChangedFiles = completion.implementation_changed_files;
|
|
877
|
+
if (implementationChangedFiles === false) {
|
|
878
|
+
if (!nonEmptyString(completion.commit_not_required_reason)) {
|
|
879
|
+
errors.push(`${label} ${id} planning-only completion must include commit_not_required_reason`);
|
|
880
|
+
}
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
if (implementationChangedFiles === true) {
|
|
884
|
+
validateRequiredLocalCommitEvidence(root, label, id, completion, evidence, errors);
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
if (options.strictCommitGate && selectedChangeId) {
|
|
888
|
+
errors.push(`${label} ${id} strict selected-change completion must set implementation_changed_files true or false`);
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
function validateRequiredLocalCommitEvidence(root, label, candidateId, completion, evidence, errors) {
|
|
892
|
+
const evidencePath = localCommitEvidencePath(completion, evidence);
|
|
893
|
+
if (!evidencePath) {
|
|
894
|
+
errors.push(`${label} ${candidateId} implementation completion must include LOCAL_COMMIT_EVIDENCE.yaml`);
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
if (isExternalRef(evidencePath) || evidencePath.startsWith("commit:")) {
|
|
898
|
+
errors.push(`${label} ${candidateId} LOCAL_COMMIT_EVIDENCE.yaml path must be repo-relative`);
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
const resolved = resolve(root, evidencePath);
|
|
902
|
+
if (resolved !== root && !resolved.startsWith(`${root}/`)) {
|
|
903
|
+
errors.push(`${label} ${candidateId} LOCAL_COMMIT_EVIDENCE.yaml path escapes repository root`);
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
if (!existsSyncSafe(resolved)) {
|
|
907
|
+
errors.push(`${label} ${candidateId} references missing LOCAL_COMMIT_EVIDENCE.yaml ${evidencePath}`);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
function localCommitEvidencePath(completion, evidence) {
|
|
911
|
+
const explicit = stringField(completion, "local_commit_evidence_path") ?? stringField(completion, "local_commit_evidence");
|
|
912
|
+
if (explicit && explicit.endsWith("LOCAL_COMMIT_EVIDENCE.yaml")) {
|
|
913
|
+
return explicit;
|
|
914
|
+
}
|
|
915
|
+
for (const item of evidence) {
|
|
916
|
+
if (typeof item === "string" && item.endsWith("LOCAL_COMMIT_EVIDENCE.yaml")) {
|
|
917
|
+
return item;
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
return null;
|
|
921
|
+
}
|
|
922
|
+
function validateLocalCommitEvidence(root, path, data, errors) {
|
|
923
|
+
if (basename(path) !== "LOCAL_COMMIT_EVIDENCE.yaml") {
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
const label = relative(root, path);
|
|
927
|
+
const planId = stringField(data, "source_plan_id") ?? stringField(data, "plan_id");
|
|
928
|
+
const candidateId = stringField(data, "source_candidate_id") ?? stringField(data, "candidate_id");
|
|
929
|
+
const selectedChangeId = stringField(data, "selected_change_id") ?? stringField(data, "change_id");
|
|
930
|
+
const commit = stringField(data, "primary_commit") ?? stringField(data, "implementation_commit") ?? stringField(data, "commit");
|
|
931
|
+
if (!planId) {
|
|
932
|
+
errors.push(`${label} missing source_plan_id or plan_id`);
|
|
933
|
+
}
|
|
934
|
+
if (!candidateId) {
|
|
935
|
+
errors.push(`${label} missing source_candidate_id or candidate_id`);
|
|
936
|
+
}
|
|
937
|
+
if (!selectedChangeId) {
|
|
938
|
+
errors.push(`${label} missing selected_change_id or change_id`);
|
|
939
|
+
}
|
|
940
|
+
if (!commit || !/^[0-9a-f]{7,40}$/i.test(commit)) {
|
|
941
|
+
errors.push(`${label} must include primary_commit or implementation_commit with a 7-40 character git hash`);
|
|
942
|
+
}
|
|
943
|
+
if (!hasValidationEvidence(data)) {
|
|
944
|
+
errors.push(`${label} must include validation_evidence, validations, or validation.commands_run`);
|
|
945
|
+
}
|
|
946
|
+
validateOptionalCoderEvidence(label, data.coder_evidence, errors);
|
|
947
|
+
}
|
|
948
|
+
function hasValidationEvidence(data) {
|
|
949
|
+
if (Array.isArray(data.validation_evidence) && data.validation_evidence.some((item) => typeof item === "string" && item.trim().length > 0)) {
|
|
950
|
+
return true;
|
|
951
|
+
}
|
|
952
|
+
if (Array.isArray(data.validations) && data.validations.some((item) => typeof item === "string" && item.trim().length > 0)) {
|
|
953
|
+
return true;
|
|
954
|
+
}
|
|
955
|
+
const validation = recordField(data, "validation");
|
|
956
|
+
return Array.isArray(validation.commands_run) && validation.commands_run.some((item) => typeof item === "string" && item.trim().length > 0);
|
|
957
|
+
}
|
|
958
|
+
function validateOptionalCoderEvidence(label, value, errors) {
|
|
959
|
+
if (value === undefined || value === null) {
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
if (!isRecord(value)) {
|
|
963
|
+
errors.push(`${label} coder_evidence must be a mapping when present`);
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
const status = stringField(value, "status");
|
|
967
|
+
if (!status) {
|
|
968
|
+
errors.push(`${label} coder_evidence.status must be recorded, skipped, or not_applicable`);
|
|
969
|
+
}
|
|
970
|
+
else if (!["recorded", "skipped", "not_applicable"].includes(status)) {
|
|
971
|
+
errors.push(`${label} coder_evidence.status has invalid value ${status}`);
|
|
972
|
+
}
|
|
973
|
+
const enforcement = stringField(value, "enforcement");
|
|
974
|
+
if (enforcement && enforcement !== "guidance_only") {
|
|
975
|
+
errors.push(`${label} coder_evidence.enforcement must be guidance_only when present`);
|
|
976
|
+
}
|
|
977
|
+
const evidenceKeys = ["preflight", "red_evidence", "green_evidence", "self_check", "validation_ladder", "lessons"];
|
|
978
|
+
let hasEvidence = false;
|
|
979
|
+
for (const key of evidenceKeys) {
|
|
980
|
+
const field = value[key];
|
|
981
|
+
if (field === undefined || field === null) {
|
|
982
|
+
continue;
|
|
983
|
+
}
|
|
984
|
+
if (!Array.isArray(field)) {
|
|
985
|
+
errors.push(`${label} coder_evidence.${key} must be a list of non-empty strings when present`);
|
|
986
|
+
continue;
|
|
987
|
+
}
|
|
988
|
+
for (const item of field) {
|
|
989
|
+
if (!nonEmptyString(item)) {
|
|
990
|
+
errors.push(`${label} coder_evidence.${key} values must be non-empty strings`);
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
if (field.some((item) => nonEmptyString(item))) {
|
|
994
|
+
hasEvidence = true;
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
if (status === "recorded" && !hasEvidence) {
|
|
998
|
+
errors.push(`${label} coder_evidence.status recorded requires at least one evidence list entry`);
|
|
999
|
+
}
|
|
1000
|
+
const notes = value.notes;
|
|
1001
|
+
if (notes !== undefined && notes !== null && !nonEmptyString(notes)) {
|
|
1002
|
+
errors.push(`${label} coder_evidence.notes must be a non-empty string when present`);
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
function validateCommonContract(root, path, data, errors) {
|
|
1006
|
+
if (!isRecord(data)) {
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
if (!("contract_type" in data) && !("schema_version" in data)) {
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
1012
|
+
const label = relative(root, path);
|
|
1013
|
+
for (const key of COMMON_REQUIRED) {
|
|
1014
|
+
if (!(key in data)) {
|
|
1015
|
+
errors.push(`${label} missing contract key ${key}`);
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
if (data.schema_version !== SCHEMA_VERSION) {
|
|
1019
|
+
errors.push(`${label} must use schema_version ${SCHEMA_VERSION}`);
|
|
1020
|
+
}
|
|
1021
|
+
for (const listKey of ["depends_on", "produces"]) {
|
|
1022
|
+
const value = data[listKey];
|
|
1023
|
+
if (Array.isArray(value)) {
|
|
1024
|
+
for (const item of value) {
|
|
1025
|
+
if (typeof item !== "string") {
|
|
1026
|
+
errors.push(`${label} has non-string ${listKey} value`);
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
function validateChange(root, path, data, errors) {
|
|
1033
|
+
if (basename(path) !== "CHANGE.yaml") {
|
|
1034
|
+
return;
|
|
1035
|
+
}
|
|
1036
|
+
for (const key of ["problem", "goals", "non_goals", "affected_paths", "acceptance", "validation"]) {
|
|
1037
|
+
if (!(key in data)) {
|
|
1038
|
+
errors.push(`${relative(root, path)} missing change key ${key}`);
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
if (data.contract_type !== "change") {
|
|
1042
|
+
errors.push(`${relative(root, path)} contract_type must be change`);
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
function validateWorkItems(root, path, data, errors) {
|
|
1046
|
+
if (basename(path) !== "WORK_ITEMS.yaml") {
|
|
1047
|
+
return;
|
|
1048
|
+
}
|
|
1049
|
+
const label = relative(root, path);
|
|
1050
|
+
if (data.contract_type !== "work_items") {
|
|
1051
|
+
errors.push(`${label} contract_type must be work_items`);
|
|
1052
|
+
}
|
|
1053
|
+
const changeContract = data.change_contract;
|
|
1054
|
+
if (typeof changeContract !== "string") {
|
|
1055
|
+
errors.push(`${label} missing change_contract`);
|
|
1056
|
+
}
|
|
1057
|
+
else if (!(existsSyncSafe(join(contractRootFor(root, path), changeContract)) || existsSyncSafe(join(root, changeContract)))) {
|
|
1058
|
+
errors.push(`${label} references missing change_contract ${changeContract}`);
|
|
1059
|
+
}
|
|
1060
|
+
const items = data.items;
|
|
1061
|
+
if (!Array.isArray(items) || items.length === 0) {
|
|
1062
|
+
errors.push(`${label} must contain non-empty items`);
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
1065
|
+
const seen = new Set();
|
|
1066
|
+
items.forEach((item, index) => {
|
|
1067
|
+
if (!isRecord(item)) {
|
|
1068
|
+
errors.push(`${label} item ${index} is not a mapping`);
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
1071
|
+
const taskId = item.task_id;
|
|
1072
|
+
if (typeof taskId !== "string" || taskId.length === 0) {
|
|
1073
|
+
errors.push(`${label} item ${index} missing task_id`);
|
|
1074
|
+
return;
|
|
1075
|
+
}
|
|
1076
|
+
if (seen.has(taskId)) {
|
|
1077
|
+
errors.push(`${label} duplicate task_id ${taskId}`);
|
|
1078
|
+
}
|
|
1079
|
+
seen.add(taskId);
|
|
1080
|
+
for (const key of ["title", "status", "owned_paths", "acceptance"]) {
|
|
1081
|
+
if (!(key in item)) {
|
|
1082
|
+
errors.push(`${label} ${taskId} missing ${key}`);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
function validateValidation(root, path, data, errors) {
|
|
1088
|
+
if (basename(path) !== "VALIDATION.yaml") {
|
|
1089
|
+
return;
|
|
1090
|
+
}
|
|
1091
|
+
const label = relative(root, path);
|
|
1092
|
+
if (data.contract_type !== "validation") {
|
|
1093
|
+
errors.push(`${label} contract_type must be validation`);
|
|
1094
|
+
}
|
|
1095
|
+
validateValidationTarget(label, data, errors);
|
|
1096
|
+
for (const key of [
|
|
1097
|
+
"core_question",
|
|
1098
|
+
"feature_classification",
|
|
1099
|
+
"critical_assumptions",
|
|
1100
|
+
"prototype_scope",
|
|
1101
|
+
"acceptance",
|
|
1102
|
+
"decision_options",
|
|
1103
|
+
]) {
|
|
1104
|
+
if (!(key in data)) {
|
|
1105
|
+
errors.push(`${label} missing validation key ${key}`);
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
const decisionOptions = data.decision_options;
|
|
1109
|
+
if (Array.isArray(decisionOptions)) {
|
|
1110
|
+
const allowed = new Set(["continue", "revise", "pivot", "stop", "needs_more_evidence"]);
|
|
1111
|
+
for (const option of decisionOptions) {
|
|
1112
|
+
if (typeof option !== "string" || !allowed.has(option)) {
|
|
1113
|
+
errors.push(`${label} has invalid decision option ${String(option)}`);
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
function validatePrototype(root, path, data, errors) {
|
|
1119
|
+
if (basename(path) !== "TODO.yaml" || data.contract_type !== "prototype") {
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
const label = relative(root, path);
|
|
1123
|
+
for (const key of [
|
|
1124
|
+
"validation_contract",
|
|
1125
|
+
"core_question",
|
|
1126
|
+
"prototype_scope",
|
|
1127
|
+
"todo",
|
|
1128
|
+
"acceptance",
|
|
1129
|
+
"artifact",
|
|
1130
|
+
"decision_handoff",
|
|
1131
|
+
]) {
|
|
1132
|
+
if (!(key in data)) {
|
|
1133
|
+
errors.push(`${label} missing prototype key ${key}`);
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
validatePrototypeScope(label, data.prototype_scope, errors);
|
|
1137
|
+
validatePrototypeTodo(label, data.todo, errors);
|
|
1138
|
+
const artifact = data.artifact;
|
|
1139
|
+
if (isRecord(artifact) && typeof artifact.path === "string" && !existsSyncSafe(join(contractRootFor(root, path), artifact.path))) {
|
|
1140
|
+
errors.push(`${label} references missing artifact path ${artifact.path}`);
|
|
1141
|
+
}
|
|
1142
|
+
const decisionHandoff = data.decision_handoff;
|
|
1143
|
+
if (isRecord(decisionHandoff) && decisionHandoff.requires_user_review !== true) {
|
|
1144
|
+
errors.push(`${label} decision_handoff.requires_user_review must be true`);
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
function validateArtifactContracts(root, path, data, errors) {
|
|
1148
|
+
if (basename(path) !== "ARTIFACT_CONTRACTS.yaml") {
|
|
1149
|
+
return;
|
|
1150
|
+
}
|
|
1151
|
+
const label = relative(root, path);
|
|
1152
|
+
const artifacts = data.artifacts;
|
|
1153
|
+
if (!Array.isArray(artifacts) || artifacts.length === 0) {
|
|
1154
|
+
errors.push(`${label} must contain artifacts`);
|
|
1155
|
+
return;
|
|
1156
|
+
}
|
|
1157
|
+
const missing = new Set([
|
|
1158
|
+
"vision_session",
|
|
1159
|
+
"validation_target",
|
|
1160
|
+
"prototype_evidence",
|
|
1161
|
+
"decision_record",
|
|
1162
|
+
"product_design",
|
|
1163
|
+
"production_spec",
|
|
1164
|
+
"production_change",
|
|
1165
|
+
"team_runtime",
|
|
1166
|
+
]);
|
|
1167
|
+
artifacts.forEach((artifact, index) => {
|
|
1168
|
+
if (!isRecord(artifact)) {
|
|
1169
|
+
errors.push(`${label} artifact ${index} is not a mapping`);
|
|
1170
|
+
return;
|
|
1171
|
+
}
|
|
1172
|
+
if (typeof artifact.artifact_type === "string") {
|
|
1173
|
+
missing.delete(artifact.artifact_type);
|
|
1174
|
+
}
|
|
1175
|
+
for (const key of [
|
|
1176
|
+
"artifact_type",
|
|
1177
|
+
"contract_type",
|
|
1178
|
+
"command",
|
|
1179
|
+
"source_of_truth_path",
|
|
1180
|
+
"template_path",
|
|
1181
|
+
"read_policy",
|
|
1182
|
+
"active_pointer",
|
|
1183
|
+
"required_keys",
|
|
1184
|
+
]) {
|
|
1185
|
+
if (!(key in artifact)) {
|
|
1186
|
+
errors.push(`${label} artifact ${index} missing ${key}`);
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
validateArtifactContractMetadata(label, index, artifact, errors);
|
|
1190
|
+
});
|
|
1191
|
+
for (const artifactType of [...missing].sort()) {
|
|
1192
|
+
errors.push(`${label} missing artifact_type ${artifactType}`);
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
function validateArtifactContractMetadata(label, index, artifact, errors) {
|
|
1196
|
+
const readPolicy = artifact.read_policy;
|
|
1197
|
+
if (isRecord(readPolicy)) {
|
|
1198
|
+
for (const key of ["load_by_default", "agent_read_order", "max_yaml_lines", "max_note_lines", "raw_evidence"]) {
|
|
1199
|
+
if (!(key in readPolicy)) {
|
|
1200
|
+
errors.push(`${label} artifact ${index} read_policy missing ${key}`);
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
else {
|
|
1205
|
+
errors.push(`${label} artifact ${index} read_policy must be a mapping`);
|
|
1206
|
+
}
|
|
1207
|
+
const activePointer = artifact.active_pointer;
|
|
1208
|
+
if (isRecord(activePointer)) {
|
|
1209
|
+
for (const key of ["index_path", "pointer_key", "collection_key", "id_key", "path_key"]) {
|
|
1210
|
+
if (!(key in activePointer)) {
|
|
1211
|
+
errors.push(`${label} artifact ${index} active_pointer missing ${key}`);
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
else {
|
|
1216
|
+
errors.push(`${label} artifact ${index} active_pointer must be a mapping`);
|
|
1217
|
+
}
|
|
1218
|
+
const summaryPolicy = artifact.summary_policy;
|
|
1219
|
+
if (summaryPolicy !== null && summaryPolicy !== undefined) {
|
|
1220
|
+
if (!isRecord(summaryPolicy)) {
|
|
1221
|
+
errors.push(`${label} artifact ${index} summary_policy must be null or a mapping`);
|
|
1222
|
+
}
|
|
1223
|
+
else {
|
|
1224
|
+
for (const key of ["strategy", "path", "load_before_full", "refresh_when"]) {
|
|
1225
|
+
if (!(key in summaryPolicy)) {
|
|
1226
|
+
errors.push(`${label} artifact ${index} summary_policy missing ${key}`);
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
function validateDisclosureLevels(root, path, data, errors) {
|
|
1233
|
+
if (basename(path) !== "DISCLOSURE_LEVELS.yaml") {
|
|
1234
|
+
return;
|
|
1235
|
+
}
|
|
1236
|
+
const label = relative(root, path);
|
|
1237
|
+
const levels = data.levels;
|
|
1238
|
+
if (!Array.isArray(levels) || levels.length < 5) {
|
|
1239
|
+
errors.push(`${label} must contain disclosure levels 0 through 4`);
|
|
1240
|
+
return;
|
|
1241
|
+
}
|
|
1242
|
+
const seen = new Set();
|
|
1243
|
+
levels.forEach((level, index) => {
|
|
1244
|
+
if (!isRecord(level)) {
|
|
1245
|
+
errors.push(`${label} level ${index} is not a mapping`);
|
|
1246
|
+
return;
|
|
1247
|
+
}
|
|
1248
|
+
if (typeof level.level === "number") {
|
|
1249
|
+
seen.add(level.level);
|
|
1250
|
+
}
|
|
1251
|
+
for (const key of ["name", "default_for_agents", "purpose", "examples"]) {
|
|
1252
|
+
if (!(key in level)) {
|
|
1253
|
+
errors.push(`${label} level ${index} missing ${key}`);
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
});
|
|
1257
|
+
for (let level = 0; level <= 4; level += 1) {
|
|
1258
|
+
if (!seen.has(level)) {
|
|
1259
|
+
errors.push(`${label} missing disclosure level ${level}`);
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
function validateDiscoveryArtifact(root, path, data, errors) {
|
|
1264
|
+
if (data.contract_type === "planning") {
|
|
1265
|
+
return;
|
|
1266
|
+
}
|
|
1267
|
+
if (typeof data.artifact_type !== "string") {
|
|
1268
|
+
return;
|
|
1269
|
+
}
|
|
1270
|
+
const label = relative(root, path);
|
|
1271
|
+
const required = artifactRequiredKeys(data.artifact_type);
|
|
1272
|
+
if (!required) {
|
|
1273
|
+
errors.push(`${label} has unknown artifact_type ${data.artifact_type}`);
|
|
1274
|
+
return;
|
|
1275
|
+
}
|
|
1276
|
+
for (const key of required) {
|
|
1277
|
+
if (!(key in data)) {
|
|
1278
|
+
errors.push(`${label} missing artifact key ${key}`);
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
if (data.artifact_type === "validation_target") {
|
|
1282
|
+
validateValidationTarget(label, data, errors);
|
|
1283
|
+
}
|
|
1284
|
+
else if (data.artifact_type === "prototype_evidence") {
|
|
1285
|
+
validatePrototypeEvidence(root, label, data, errors);
|
|
1286
|
+
}
|
|
1287
|
+
else if (data.artifact_type === "decision_record") {
|
|
1288
|
+
validateDecisionRecord(label, data, errors);
|
|
1289
|
+
}
|
|
1290
|
+
else if (data.artifact_type === "product_design") {
|
|
1291
|
+
validateProductDesign(label, data, errors);
|
|
1292
|
+
}
|
|
1293
|
+
else if (data.artifact_type === "production_spec") {
|
|
1294
|
+
validateProductionSpec(label, data, errors);
|
|
1295
|
+
}
|
|
1296
|
+
else if (data.artifact_type === "production_change") {
|
|
1297
|
+
validateProductionChange(label, data, errors);
|
|
1298
|
+
}
|
|
1299
|
+
else if (data.artifact_type === "team_runtime") {
|
|
1300
|
+
validateTeamRuntime(label, data, errors);
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
function artifactRequiredKeys(artifactType) {
|
|
1304
|
+
const requiredByType = {
|
|
1305
|
+
vision_session: ["current_question", "stable_answers", "unresolved_questions", "vision_delta", "handoff"],
|
|
1306
|
+
validation_target: [
|
|
1307
|
+
"trigger",
|
|
1308
|
+
"core_question",
|
|
1309
|
+
"central_uncertainty",
|
|
1310
|
+
"hypothesis",
|
|
1311
|
+
"target_behavior",
|
|
1312
|
+
"feature_classification",
|
|
1313
|
+
"critical_assumptions",
|
|
1314
|
+
"prototype_scope",
|
|
1315
|
+
"prototype_experiment",
|
|
1316
|
+
"observable_signals",
|
|
1317
|
+
"acceptance",
|
|
1318
|
+
"decision_rules",
|
|
1319
|
+
"decision_options",
|
|
1320
|
+
"vision_gaps",
|
|
1321
|
+
"agent_readiness_gate",
|
|
1322
|
+
],
|
|
1323
|
+
prototype_evidence: [
|
|
1324
|
+
"validation_target",
|
|
1325
|
+
"core_question",
|
|
1326
|
+
"prototype_mode",
|
|
1327
|
+
"prompt_pack_type",
|
|
1328
|
+
"validation_input",
|
|
1329
|
+
"source",
|
|
1330
|
+
"negative_constraints",
|
|
1331
|
+
"review_plan",
|
|
1332
|
+
"result",
|
|
1333
|
+
"handoff",
|
|
1334
|
+
],
|
|
1335
|
+
decision_record: [
|
|
1336
|
+
"reviewed_evidence",
|
|
1337
|
+
"outcome",
|
|
1338
|
+
"rationale",
|
|
1339
|
+
"accepted_scope",
|
|
1340
|
+
"rejected_scope",
|
|
1341
|
+
"revision_scope",
|
|
1342
|
+
"next_command",
|
|
1343
|
+
"follow_up_questions",
|
|
1344
|
+
],
|
|
1345
|
+
product_design: [
|
|
1346
|
+
"accepted_prototype_evidence",
|
|
1347
|
+
"personas",
|
|
1348
|
+
"journey_map",
|
|
1349
|
+
"user_stories",
|
|
1350
|
+
"feature_matrix",
|
|
1351
|
+
"kano_classification",
|
|
1352
|
+
"behavior_model",
|
|
1353
|
+
"ux_states",
|
|
1354
|
+
"scope",
|
|
1355
|
+
"open_questions",
|
|
1356
|
+
"conditional_packets",
|
|
1357
|
+
"spec_readiness",
|
|
1358
|
+
],
|
|
1359
|
+
production_spec: [
|
|
1360
|
+
"source_design",
|
|
1361
|
+
"goal",
|
|
1362
|
+
"scope",
|
|
1363
|
+
"requirements",
|
|
1364
|
+
"interfaces",
|
|
1365
|
+
"acceptance",
|
|
1366
|
+
"verification",
|
|
1367
|
+
"risks",
|
|
1368
|
+
"change_readiness",
|
|
1369
|
+
],
|
|
1370
|
+
production_change: [
|
|
1371
|
+
"source_spec",
|
|
1372
|
+
"problem",
|
|
1373
|
+
"goals",
|
|
1374
|
+
"non_goals",
|
|
1375
|
+
"affected_paths",
|
|
1376
|
+
"acceptance",
|
|
1377
|
+
"validation",
|
|
1378
|
+
"work_items",
|
|
1379
|
+
"risks",
|
|
1380
|
+
"runtime_readiness",
|
|
1381
|
+
],
|
|
1382
|
+
team_runtime: [
|
|
1383
|
+
"source_change",
|
|
1384
|
+
"active_work_item",
|
|
1385
|
+
"execution_mode",
|
|
1386
|
+
"work_queue",
|
|
1387
|
+
"agents",
|
|
1388
|
+
"verification",
|
|
1389
|
+
"issues",
|
|
1390
|
+
"checkpoints",
|
|
1391
|
+
"handoff",
|
|
1392
|
+
],
|
|
1393
|
+
};
|
|
1394
|
+
return requiredByType[artifactType] ?? null;
|
|
1395
|
+
}
|
|
1396
|
+
function validateValidationTarget(label, data, errors) {
|
|
1397
|
+
validateValidationTrigger(label, data.trigger, errors);
|
|
1398
|
+
const featureClassification = data.feature_classification;
|
|
1399
|
+
if (isRecord(featureClassification)) {
|
|
1400
|
+
for (const key of ["existential", "supporting", "later", "out_of_scope"]) {
|
|
1401
|
+
if (!(key in featureClassification)) {
|
|
1402
|
+
errors.push(`${label} feature_classification missing ${key}`);
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
validatePrototypeScope(label, data.prototype_scope, errors);
|
|
1407
|
+
validatePrototypeExperiment(label, data.prototype_experiment, errors);
|
|
1408
|
+
validateSignalSet(label, "observable_signals", data.observable_signals, ["pass", "fail", "ambiguous"], errors);
|
|
1409
|
+
validateSignalSet(label, "decision_rules", data.decision_rules, ["continue", "revise", "pivot", "stop", "needs_more_evidence"], errors);
|
|
1410
|
+
validateAgentReadinessGate(label, data.agent_readiness_gate, errors);
|
|
1411
|
+
}
|
|
1412
|
+
function validateValidationTrigger(label, value, errors) {
|
|
1413
|
+
if (!isRecord(value)) {
|
|
1414
|
+
return;
|
|
1415
|
+
}
|
|
1416
|
+
for (const key of ["mode", "requested_command", "reason"]) {
|
|
1417
|
+
if (!(key in value)) {
|
|
1418
|
+
errors.push(`${label} trigger missing ${key}`);
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
const mode = String(value.mode ?? "");
|
|
1422
|
+
if (mode && !["user_explicit", "agent_auto"].includes(mode)) {
|
|
1423
|
+
errors.push(`${label} trigger.mode has invalid value ${mode}`);
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
function validatePrototypeExperiment(label, value, errors) {
|
|
1427
|
+
if (!isRecord(value)) {
|
|
1428
|
+
return;
|
|
1429
|
+
}
|
|
1430
|
+
for (const key of ["scenario", "must_show", "must_not_show"]) {
|
|
1431
|
+
if (!(key in value)) {
|
|
1432
|
+
errors.push(`${label} prototype_experiment missing ${key}`);
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
function validateSignalSet(label, field, value, keys, errors) {
|
|
1437
|
+
if (!isRecord(value)) {
|
|
1438
|
+
return;
|
|
1439
|
+
}
|
|
1440
|
+
for (const key of keys) {
|
|
1441
|
+
if (!(key in value)) {
|
|
1442
|
+
errors.push(`${label} ${field} missing ${key}`);
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
function validateAgentReadinessGate(label, value, errors) {
|
|
1447
|
+
if (!isRecord(value)) {
|
|
1448
|
+
return;
|
|
1449
|
+
}
|
|
1450
|
+
for (const key of ["status", "blockers", "warnings", "write_authority"]) {
|
|
1451
|
+
if (!(key in value)) {
|
|
1452
|
+
errors.push(`${label} agent_readiness_gate missing ${key}`);
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
const status = String(value.status ?? "");
|
|
1456
|
+
if (status && !["missing_validation", "thin_validation", "stale_validation", "ready_for_proto", "return_to_vision"].includes(status)) {
|
|
1457
|
+
errors.push(`${label} agent_readiness_gate.status has invalid value ${status}`);
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
function validatePrototypeScope(label, value, errors) {
|
|
1461
|
+
if (!isRecord(value)) {
|
|
1462
|
+
return;
|
|
1463
|
+
}
|
|
1464
|
+
for (const key of ["include", "exclude"]) {
|
|
1465
|
+
if (!(key in value)) {
|
|
1466
|
+
errors.push(`${label} prototype_scope missing ${key}`);
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
function validatePrototypeTodo(label, value, errors) {
|
|
1471
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
1472
|
+
errors.push(`${label} must contain non-empty todo list`);
|
|
1473
|
+
return;
|
|
1474
|
+
}
|
|
1475
|
+
const seen = new Set();
|
|
1476
|
+
value.forEach((item, index) => {
|
|
1477
|
+
if (!isRecord(item)) {
|
|
1478
|
+
errors.push(`${label} todo item ${index} is not a mapping`);
|
|
1479
|
+
return;
|
|
1480
|
+
}
|
|
1481
|
+
const taskId = item.task_id;
|
|
1482
|
+
if (typeof taskId !== "string" || taskId.length === 0) {
|
|
1483
|
+
errors.push(`${label} todo item ${index} missing task_id`);
|
|
1484
|
+
return;
|
|
1485
|
+
}
|
|
1486
|
+
if (seen.has(taskId)) {
|
|
1487
|
+
errors.push(`${label} duplicate prototype task_id ${taskId}`);
|
|
1488
|
+
}
|
|
1489
|
+
seen.add(taskId);
|
|
1490
|
+
for (const key of ["title", "status", "acceptance"]) {
|
|
1491
|
+
if (!(key in item)) {
|
|
1492
|
+
errors.push(`${label} ${taskId} missing ${key}`);
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
});
|
|
1496
|
+
}
|
|
1497
|
+
function validatePrototypeEvidence(root, label, data, errors) {
|
|
1498
|
+
if (!["image_prompt_pack", "visual", "interaction", "technical_feasibility", "3d_material", "workflow", "data_logic"].includes(String(data.prototype_mode))) {
|
|
1499
|
+
errors.push(`${label} has invalid prototype_mode ${String(data.prototype_mode)}`);
|
|
1500
|
+
}
|
|
1501
|
+
for (const key of ["reference_analysis", "concept_evidence", "implementation_evidence", "known_limits"]) {
|
|
1502
|
+
if (key in data && !Array.isArray(data[key])) {
|
|
1503
|
+
errors.push(`${label} ${key} must be an array`);
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
if ("visual_direction" in data && !isRecord(data.visual_direction)) {
|
|
1507
|
+
errors.push(`${label} visual_direction must be a mapping`);
|
|
1508
|
+
}
|
|
1509
|
+
if ("visual_concept_policy" in data) {
|
|
1510
|
+
validateVisualConceptPolicy(label, data, errors);
|
|
1511
|
+
}
|
|
1512
|
+
if ("verification" in data && !isRecord(data.verification)) {
|
|
1513
|
+
errors.push(`${label} verification must be a mapping`);
|
|
1514
|
+
}
|
|
1515
|
+
if ("self_critique" in data && !isRecord(data.self_critique)) {
|
|
1516
|
+
errors.push(`${label} self_critique must be a mapping`);
|
|
1517
|
+
}
|
|
1518
|
+
else {
|
|
1519
|
+
validateSelfCritique(label, data.self_critique, errors);
|
|
1520
|
+
}
|
|
1521
|
+
const prototypeArtifact = data.prototype_artifact;
|
|
1522
|
+
if (isRecord(prototypeArtifact)) {
|
|
1523
|
+
for (const key of ["path", "type"]) {
|
|
1524
|
+
if (!(key in prototypeArtifact)) {
|
|
1525
|
+
errors.push(`${label} prototype_artifact missing ${key}`);
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
validateLocalRef(root, label, "prototype_artifact.path", prototypeArtifact.path, errors);
|
|
1529
|
+
}
|
|
1530
|
+
validateEvidenceRefs(root, label, data, errors);
|
|
1531
|
+
if ("validation_input" in data && !isRecord(data.validation_input)) {
|
|
1532
|
+
errors.push(`${label} validation_input must be a mapping`);
|
|
1533
|
+
}
|
|
1534
|
+
else {
|
|
1535
|
+
validatePrototypeValidationInput(label, data.validation_input, errors);
|
|
1536
|
+
}
|
|
1537
|
+
if ("source" in data && !isRecord(data.source)) {
|
|
1538
|
+
errors.push(`${label} source must be a mapping`);
|
|
1539
|
+
}
|
|
1540
|
+
if ("negative_constraints" in data && !Array.isArray(data.negative_constraints)) {
|
|
1541
|
+
errors.push(`${label} negative_constraints must be an array`);
|
|
1542
|
+
}
|
|
1543
|
+
if ("review_plan" in data && !isRecord(data.review_plan)) {
|
|
1544
|
+
errors.push(`${label} review_plan must be a mapping`);
|
|
1545
|
+
}
|
|
1546
|
+
if ("directions" in data && !Array.isArray(data.directions)) {
|
|
1547
|
+
errors.push(`${label} directions must be an array`);
|
|
1548
|
+
}
|
|
1549
|
+
if ("screen_manifest" in data && !Array.isArray(data.screen_manifest)) {
|
|
1550
|
+
errors.push(`${label} screen_manifest must be an array`);
|
|
1551
|
+
}
|
|
1552
|
+
if ("screen_prompts" in data && !Array.isArray(data.screen_prompts)) {
|
|
1553
|
+
errors.push(`${label} screen_prompts must be an array`);
|
|
1554
|
+
}
|
|
1555
|
+
if ("prompt_pack_type" in data && !["strategic_proto_prompt_pack", "refined_proto_prompt_pack", "proto_review_evidence"].includes(String(data.prompt_pack_type))) {
|
|
1556
|
+
errors.push(`${label} has invalid prompt_pack_type ${String(data.prompt_pack_type)}`);
|
|
1557
|
+
}
|
|
1558
|
+
if (data.prompt_pack_type === "strategic_proto_prompt_pack") {
|
|
1559
|
+
validateStrategicPrototypePromptPack(label, data, errors);
|
|
1560
|
+
}
|
|
1561
|
+
if (data.prompt_pack_type === "refined_proto_prompt_pack") {
|
|
1562
|
+
validateRefinedPrototypePromptPack(label, data, errors);
|
|
1563
|
+
}
|
|
1564
|
+
if (!["pass", "fail", "unclear", "not_reviewed"].includes(String(data.result))) {
|
|
1565
|
+
errors.push(`${label} has invalid result ${String(data.result)}`);
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
function validateRefinedPrototypePromptPack(label, data, errors) {
|
|
1569
|
+
validateRequiredObjectFields(label, "tune_input", data.tune_input, ["baseline_source_type", "baseline_refs", "tune_request", "regeneration_scope"], errors);
|
|
1570
|
+
validateRefinedBaselineResolution(label, data.baseline_resolution, errors);
|
|
1571
|
+
validateRefinedCarryForward(label, data.carry_forward, errors);
|
|
1572
|
+
validateRefinedBaselineAudit(label, data.baseline_audit, errors);
|
|
1573
|
+
validateRequiredObjectFields(label, "product_system", data.product_system, REFINED_PRODUCT_SYSTEM_FIELDS, errors);
|
|
1574
|
+
validateRefinedDeltaRules(label, data.delta_rules, data.tune_input, errors);
|
|
1575
|
+
validateRefinedScreenDeltaMatrix(label, data.screen_delta_matrix, errors);
|
|
1576
|
+
const manifestIds = validateRefinedScreenManifest(label, data.screen_manifest, errors);
|
|
1577
|
+
validateRefinedScreenPrompts(label, data.screen_prompts, manifestIds, errors);
|
|
1578
|
+
if (!Array.isArray(data.generation_order) || data.generation_order.length === 0) {
|
|
1579
|
+
errors.push(`${label} generation_order must list target screen ids`);
|
|
1580
|
+
}
|
|
1581
|
+
if (!Array.isArray(data.acceptance_checklist) || data.acceptance_checklist.length === 0) {
|
|
1582
|
+
errors.push(`${label} acceptance_checklist must be non-empty`);
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
const REFINED_BASELINE_AUDIT_FIELDS = [
|
|
1586
|
+
"source_screen_id",
|
|
1587
|
+
"screen_name",
|
|
1588
|
+
"journey_stage",
|
|
1589
|
+
"user_goal",
|
|
1590
|
+
"system_state",
|
|
1591
|
+
"components",
|
|
1592
|
+
"must_preserve",
|
|
1593
|
+
];
|
|
1594
|
+
const REFINED_BASELINE_RESOLUTION_FIELDS = [
|
|
1595
|
+
"latest_approved_baseline_group_id",
|
|
1596
|
+
"latest_approved_baseline_ref",
|
|
1597
|
+
"baseline_lineage",
|
|
1598
|
+
"resolution_rule",
|
|
1599
|
+
"stale_source_guard",
|
|
1600
|
+
];
|
|
1601
|
+
const REFINED_CARRY_FORWARD_FIELDS = [
|
|
1602
|
+
"locked_screens",
|
|
1603
|
+
"locked_elements",
|
|
1604
|
+
"preserved_improvements",
|
|
1605
|
+
"explicit_unlocks",
|
|
1606
|
+
"cumulative_drift_guard",
|
|
1607
|
+
];
|
|
1608
|
+
const REFINED_PRODUCT_SYSTEM_FIELDS = [
|
|
1609
|
+
"product_thesis",
|
|
1610
|
+
"primary_loop",
|
|
1611
|
+
"component_vocabulary",
|
|
1612
|
+
"copywriting_style",
|
|
1613
|
+
"trust_and_boundary_system",
|
|
1614
|
+
"stable_constants",
|
|
1615
|
+
"adaptable_variables",
|
|
1616
|
+
];
|
|
1617
|
+
const REFINED_DELTA_RULE_KEYS = ["must_inherit", "must_add", "must_remove", "flexible_change"];
|
|
1618
|
+
const REFINED_SCREEN_DELTA_FIELDS = [
|
|
1619
|
+
"target_screen_id",
|
|
1620
|
+
"source_screen_ids",
|
|
1621
|
+
"preserve",
|
|
1622
|
+
"add",
|
|
1623
|
+
"remove",
|
|
1624
|
+
"transform",
|
|
1625
|
+
"flexible",
|
|
1626
|
+
"acceptance_criteria",
|
|
1627
|
+
];
|
|
1628
|
+
const REFINED_SCREEN_MANIFEST_FIELDS = ["target_screen_id", "source_screen_ids", "screen_name", "generation_scope"];
|
|
1629
|
+
const REFINED_SCREEN_PROMPT_FIELDS = ["prompt_id", "target_screen_id", "source_screen_ids", "screen_name", "prompt", "negative_prompt", "acceptance_criteria"];
|
|
1630
|
+
function validateRefinedBaselineAudit(label, value, errors) {
|
|
1631
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
1632
|
+
errors.push(`${label} baseline_audit must contain source screen audits`);
|
|
1633
|
+
return;
|
|
1634
|
+
}
|
|
1635
|
+
value.forEach((item, index) => {
|
|
1636
|
+
validateRequiredObjectFields(label, `baseline_audit[${index}]`, item, REFINED_BASELINE_AUDIT_FIELDS, errors);
|
|
1637
|
+
});
|
|
1638
|
+
}
|
|
1639
|
+
function validateRefinedBaselineResolution(label, value, errors) {
|
|
1640
|
+
validateRequiredObjectFields(label, "baseline_resolution", value, REFINED_BASELINE_RESOLUTION_FIELDS, errors);
|
|
1641
|
+
if (!isRecord(value)) {
|
|
1642
|
+
return;
|
|
1643
|
+
}
|
|
1644
|
+
if (!Array.isArray(value.baseline_lineage)) {
|
|
1645
|
+
errors.push(`${label} baseline_resolution.baseline_lineage must be an array`);
|
|
1646
|
+
}
|
|
1647
|
+
if (String(value.stale_source_guard ?? "").trim().length === 0) {
|
|
1648
|
+
errors.push(`${label} baseline_resolution.stale_source_guard must forbid stale source-screen fallback`);
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
function validateRefinedCarryForward(label, value, errors) {
|
|
1652
|
+
if (!isRecord(value)) {
|
|
1653
|
+
errors.push(`${label} carry_forward must be a mapping`);
|
|
1654
|
+
return;
|
|
1655
|
+
}
|
|
1656
|
+
for (const key of REFINED_CARRY_FORWARD_FIELDS) {
|
|
1657
|
+
if (!Object.hasOwn(value, key)) {
|
|
1658
|
+
errors.push(`${label} carry_forward.${key} must be present`);
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
for (const key of ["locked_screens", "locked_elements", "preserved_improvements", "explicit_unlocks"]) {
|
|
1662
|
+
if (!Array.isArray(value[key])) {
|
|
1663
|
+
errors.push(`${label} carry_forward.${key} must be an array`);
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
if (String(value.cumulative_drift_guard ?? "").trim().length === 0) {
|
|
1667
|
+
errors.push(`${label} carry_forward.cumulative_drift_guard must forbid cumulative tune drift`);
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
function validateRefinedDeltaRules(label, value, tuneInput, errors) {
|
|
1671
|
+
if (!isRecord(value)) {
|
|
1672
|
+
errors.push(`${label} delta_rules must be a mapping`);
|
|
1673
|
+
return;
|
|
1674
|
+
}
|
|
1675
|
+
for (const key of REFINED_DELTA_RULE_KEYS) {
|
|
1676
|
+
if (!Array.isArray(value[key])) {
|
|
1677
|
+
errors.push(`${label} delta_rules.${key} must be an array`);
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
if (Array.isArray(value.must_inherit) && value.must_inherit.length === 0) {
|
|
1681
|
+
errors.push(`${label} delta_rules.must_inherit must preserve baseline product-system constants`);
|
|
1682
|
+
}
|
|
1683
|
+
const tuneRequest = isRecord(tuneInput) ? String(tuneInput.tune_request ?? "").toLowerCase() : "";
|
|
1684
|
+
if (/\b(remove|delete|drop|eliminate|hide)\b/.test(tuneRequest) && Array.isArray(value.must_remove) && value.must_remove.length === 0) {
|
|
1685
|
+
errors.push(`${label} delta_rules.must_remove must name requested removals from tune_input.tune_request`);
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
function validateRefinedScreenDeltaMatrix(label, value, errors) {
|
|
1689
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
1690
|
+
errors.push(`${label} screen_delta_matrix must contain target screen delta rows`);
|
|
1691
|
+
return;
|
|
1692
|
+
}
|
|
1693
|
+
value.forEach((item, index) => {
|
|
1694
|
+
validateRequiredObjectFields(label, `screen_delta_matrix[${index}]`, item, REFINED_SCREEN_DELTA_FIELDS, errors);
|
|
1695
|
+
});
|
|
1696
|
+
}
|
|
1697
|
+
function validateRefinedScreenManifest(label, value, errors) {
|
|
1698
|
+
const ids = new Set();
|
|
1699
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
1700
|
+
errors.push(`${label} screen_manifest must contain target screens`);
|
|
1701
|
+
return ids;
|
|
1702
|
+
}
|
|
1703
|
+
value.forEach((item, index) => {
|
|
1704
|
+
validateRequiredObjectFields(label, `screen_manifest[${index}]`, item, REFINED_SCREEN_MANIFEST_FIELDS, errors);
|
|
1705
|
+
if (isRecord(item) && nonEmptyString(item.target_screen_id)) {
|
|
1706
|
+
ids.add(String(item.target_screen_id));
|
|
1707
|
+
}
|
|
1708
|
+
});
|
|
1709
|
+
return ids;
|
|
1710
|
+
}
|
|
1711
|
+
function validateRefinedScreenPrompts(label, value, manifestIds, errors) {
|
|
1712
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
1713
|
+
errors.push(`${label} screen_prompts must contain screen-bound refined prompts`);
|
|
1714
|
+
return;
|
|
1715
|
+
}
|
|
1716
|
+
value.forEach((item, index) => {
|
|
1717
|
+
validateRequiredObjectFields(label, `screen_prompts[${index}]`, item, REFINED_SCREEN_PROMPT_FIELDS, errors);
|
|
1718
|
+
if (isRecord(item) && nonEmptyString(item.target_screen_id) && manifestIds.size > 0 && !manifestIds.has(String(item.target_screen_id))) {
|
|
1719
|
+
errors.push(`${label} screen_prompts[${index}].target_screen_id must exist in screen_manifest`);
|
|
1720
|
+
}
|
|
1721
|
+
});
|
|
1722
|
+
}
|
|
1723
|
+
const STRATEGIC_NORMALIZED_FIELDS = [
|
|
1724
|
+
"product_domain",
|
|
1725
|
+
"primary_user",
|
|
1726
|
+
"usage_context",
|
|
1727
|
+
"current_alternative",
|
|
1728
|
+
"core_pain",
|
|
1729
|
+
"desired_behavior_change",
|
|
1730
|
+
"strongest_success_signal",
|
|
1731
|
+
"core_differentiator",
|
|
1732
|
+
"emotional_value",
|
|
1733
|
+
"functional_value",
|
|
1734
|
+
"trust_requirements",
|
|
1735
|
+
"privacy_requirements",
|
|
1736
|
+
"non_goals",
|
|
1737
|
+
"future_opportunities",
|
|
1738
|
+
"validation_target",
|
|
1739
|
+
];
|
|
1740
|
+
const STRATEGIC_CORE_FIELDS = [
|
|
1741
|
+
"target_user",
|
|
1742
|
+
"behavior_change",
|
|
1743
|
+
"mechanism",
|
|
1744
|
+
"differentiator",
|
|
1745
|
+
"boundary_conditions",
|
|
1746
|
+
"central_uncertainty",
|
|
1747
|
+
];
|
|
1748
|
+
const PRODUCT_EXPERIENCE_MODEL_FIELDS = [
|
|
1749
|
+
"product_archetype",
|
|
1750
|
+
"primary_canvas",
|
|
1751
|
+
"information_architecture",
|
|
1752
|
+
"domain_object_model",
|
|
1753
|
+
"primary_task_loop",
|
|
1754
|
+
"interaction_state_model",
|
|
1755
|
+
"data_realism_requirements",
|
|
1756
|
+
"visual_language",
|
|
1757
|
+
"anti_generic_constraints",
|
|
1758
|
+
];
|
|
1759
|
+
const PROTOTYPE_SYSTEM_CONTRACT_FIELDS = [
|
|
1760
|
+
"stable_app_shell",
|
|
1761
|
+
"navigation_taxonomy",
|
|
1762
|
+
"data_vocabulary",
|
|
1763
|
+
"domain_object_anatomy",
|
|
1764
|
+
"object_detail_anatomy",
|
|
1765
|
+
"action_bar_contract",
|
|
1766
|
+
"audit_trust_pattern",
|
|
1767
|
+
"copy_tone",
|
|
1768
|
+
"allowed_screen_deltas",
|
|
1769
|
+
];
|
|
1770
|
+
const PROTOTYPE_REALITY_GATE_DIMENSIONS = [
|
|
1771
|
+
"product_category_fit",
|
|
1772
|
+
"primary_canvas_fit",
|
|
1773
|
+
"domain_object_realism",
|
|
1774
|
+
"task_loop_completeness",
|
|
1775
|
+
"interaction_state_coverage",
|
|
1776
|
+
"data_realism",
|
|
1777
|
+
"anti_generic_constraints",
|
|
1778
|
+
];
|
|
1779
|
+
const PROMPT_PACK_INTEGRITY_GATE_DIMENSIONS = [
|
|
1780
|
+
"direction_count_matches",
|
|
1781
|
+
"prompt_text_refs_resolve",
|
|
1782
|
+
"generated_image_refs_resolve",
|
|
1783
|
+
];
|
|
1784
|
+
const STRATEGIC_PROTOTYPE_BRIEF_FIELDS = [
|
|
1785
|
+
"product_name",
|
|
1786
|
+
"positioning",
|
|
1787
|
+
"target_user",
|
|
1788
|
+
"current_alternative",
|
|
1789
|
+
"core_idea",
|
|
1790
|
+
"primary_loop",
|
|
1791
|
+
"trust_boundaries",
|
|
1792
|
+
"non_goals",
|
|
1793
|
+
"desired_feeling",
|
|
1794
|
+
];
|
|
1795
|
+
const STRATEGIC_SCREEN_MANIFEST_FIELDS = [
|
|
1796
|
+
"target_screen_id",
|
|
1797
|
+
"screen_name",
|
|
1798
|
+
"journey_stage",
|
|
1799
|
+
"user_goal",
|
|
1800
|
+
"system_state",
|
|
1801
|
+
"required_components",
|
|
1802
|
+
"required_data_fields",
|
|
1803
|
+
"primary_actions",
|
|
1804
|
+
"trust_controls",
|
|
1805
|
+
"example_copy",
|
|
1806
|
+
"acceptance_criteria",
|
|
1807
|
+
];
|
|
1808
|
+
const STRATEGIC_GLOBAL_DESIGN_SYSTEM_PROMPT_FIELDS = [
|
|
1809
|
+
"visual_language",
|
|
1810
|
+
"layout_system",
|
|
1811
|
+
"component_vocabulary",
|
|
1812
|
+
"information_density",
|
|
1813
|
+
"copy_tone",
|
|
1814
|
+
"responsive_canvas_rules",
|
|
1815
|
+
"negative_visual_patterns",
|
|
1816
|
+
];
|
|
1817
|
+
const STRATEGIC_SCREEN_PROMPT_FIELDS = [
|
|
1818
|
+
"prompt_id",
|
|
1819
|
+
"target_screen_id",
|
|
1820
|
+
"screen_name",
|
|
1821
|
+
"image_role",
|
|
1822
|
+
"negative_prompt",
|
|
1823
|
+
"example_copy",
|
|
1824
|
+
"acceptance_criteria",
|
|
1825
|
+
];
|
|
1826
|
+
const STRATEGIC_QUALITY_RUBRIC_FIELDS = [
|
|
1827
|
+
"prompt_executability",
|
|
1828
|
+
"strategic_distinctness",
|
|
1829
|
+
"product_specificity",
|
|
1830
|
+
"state_coverage",
|
|
1831
|
+
"trust_boundary_coverage",
|
|
1832
|
+
];
|
|
1833
|
+
const STRATEGIC_DIRECTION_FIELDS = [
|
|
1834
|
+
"direction_id",
|
|
1835
|
+
"name",
|
|
1836
|
+
"strategic_hypothesis",
|
|
1837
|
+
"validates",
|
|
1838
|
+
"main_risk",
|
|
1839
|
+
"distinctness_rationale",
|
|
1840
|
+
"prototype_prompt",
|
|
1841
|
+
"screen_prompts",
|
|
1842
|
+
"pm_judgment",
|
|
1843
|
+
];
|
|
1844
|
+
const STRATEGIC_DISTINCTNESS_SIGNALS = [
|
|
1845
|
+
"product form",
|
|
1846
|
+
"trigger",
|
|
1847
|
+
"interaction model",
|
|
1848
|
+
"emotional driver",
|
|
1849
|
+
"retention mechanism",
|
|
1850
|
+
"metric",
|
|
1851
|
+
"main risk",
|
|
1852
|
+
"risk",
|
|
1853
|
+
"user behavior",
|
|
1854
|
+
"workflow",
|
|
1855
|
+
"trust",
|
|
1856
|
+
"privacy",
|
|
1857
|
+
];
|
|
1858
|
+
const STRATEGIC_FINGERPRINT_DIMENSIONS = [
|
|
1859
|
+
"product_form",
|
|
1860
|
+
"trigger",
|
|
1861
|
+
"interaction_model",
|
|
1862
|
+
"emotional_driver",
|
|
1863
|
+
"retention_mechanism",
|
|
1864
|
+
"metric",
|
|
1865
|
+
"main_risk",
|
|
1866
|
+
"trust_model",
|
|
1867
|
+
"privacy_model",
|
|
1868
|
+
];
|
|
1869
|
+
function validatePrototypeValidationInput(label, value, errors) {
|
|
1870
|
+
if (!isRecord(value)) {
|
|
1871
|
+
return;
|
|
1872
|
+
}
|
|
1873
|
+
const mode = String(value.mode ?? "");
|
|
1874
|
+
if (mode && !["validation_present", "agent_auto_generated"].includes(mode)) {
|
|
1875
|
+
errors.push(`${label} validation_input.mode has invalid value ${mode}`);
|
|
1876
|
+
}
|
|
1877
|
+
if (mode === "vision_only" || mode === "internally_derived") {
|
|
1878
|
+
errors.push(`${label} validation_input.mode must reference durable validation, not ${mode}`);
|
|
1879
|
+
}
|
|
1880
|
+
if (!Array.isArray(value.refs)) {
|
|
1881
|
+
errors.push(`${label} validation_input.refs must be an array`);
|
|
1882
|
+
}
|
|
1883
|
+
else if (mode && value.refs.length === 0) {
|
|
1884
|
+
errors.push(`${label} validation_input.refs must include durable validation artifact refs`);
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
function validateStrategicPrototypePromptPack(label, data, errors) {
|
|
1888
|
+
validatePreflightQualityGate(label, data.preflight_quality_gate, errors);
|
|
1889
|
+
validateInternalPipeline(label, data.internal_pipeline, errors);
|
|
1890
|
+
validateDirectionCountPolicy(label, data.direction_count_policy, errors);
|
|
1891
|
+
validateRequiredObjectFields(label, "normalized_input", data.normalized_input, STRATEGIC_NORMALIZED_FIELDS, errors);
|
|
1892
|
+
validateRequiredObjectFields(label, "strategic_core", data.strategic_core, STRATEGIC_CORE_FIELDS, errors);
|
|
1893
|
+
validateProductExperienceModel(label, data.product_experience_model, data, errors);
|
|
1894
|
+
validatePrototypeSystemContract(label, data.prototype_system_contract, data, errors);
|
|
1895
|
+
validatePrototypeRealityGate(label, data.prototype_reality_gate, data, errors);
|
|
1896
|
+
validatePromptPackIntegrityGate(label, data.prompt_pack_integrity_gate, data, errors);
|
|
1897
|
+
validateScreenBoundExecutability(label, data, errors);
|
|
1898
|
+
validateStrategicDirections(label, data.directions, data.direction_count_policy, errors);
|
|
1899
|
+
validateBuildRecommendation(label, data.build_recommendation, errors);
|
|
1900
|
+
validatePromptTextManifest(label, data.prompt_text_manifest, errors);
|
|
1901
|
+
validatePostValidate(label, data.post_validate, data.direction_count_policy, data.prompt_text_manifest, data.image_generation, data.directions, errors);
|
|
1902
|
+
validateImageGeneration(label, data.image_generation, errors);
|
|
1903
|
+
}
|
|
1904
|
+
function validatePreflightQualityGate(label, value, errors) {
|
|
1905
|
+
if (!isRecord(value)) {
|
|
1906
|
+
errors.push(`${label} preflight_quality_gate must be a mapping`);
|
|
1907
|
+
return;
|
|
1908
|
+
}
|
|
1909
|
+
for (const key of ["vision_status", "validation_status", "can_proceed", "blockers", "next_command_when_blocked"]) {
|
|
1910
|
+
if (!(key in value)) {
|
|
1911
|
+
errors.push(`${label} preflight_quality_gate missing ${key}`);
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
for (const key of ["vision_status", "validation_status"]) {
|
|
1915
|
+
const status = String(value[key] ?? "");
|
|
1916
|
+
if (status && !["missing", "thin", "ready"].includes(status)) {
|
|
1917
|
+
errors.push(`${label} preflight_quality_gate.${key} has invalid value ${status}`);
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
if (value.can_proceed !== true && value.next_command_when_blocked !== "/ow:vision") {
|
|
1921
|
+
errors.push(`${label} preflight_quality_gate must route blocked prototype work back to /ow:vision`);
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
function validateInternalPipeline(label, value, errors) {
|
|
1925
|
+
if (!isRecord(value)) {
|
|
1926
|
+
errors.push(`${label} internal_pipeline must be a mapping`);
|
|
1927
|
+
return;
|
|
1928
|
+
}
|
|
1929
|
+
if (value.orchestrator_command !== "/ow:proto" || value.user_visible_command !== "/ow:proto") {
|
|
1930
|
+
errors.push(`${label} internal_pipeline must keep /ow:proto as the user-visible orchestrator`);
|
|
1931
|
+
}
|
|
1932
|
+
const stages = value.stages;
|
|
1933
|
+
if (!Array.isArray(stages)) {
|
|
1934
|
+
errors.push(`${label} internal_pipeline.stages must be an array`);
|
|
1935
|
+
return;
|
|
1936
|
+
}
|
|
1937
|
+
const stageIds = new Set();
|
|
1938
|
+
for (const item of stages) {
|
|
1939
|
+
if (!isRecord(item)) {
|
|
1940
|
+
errors.push(`${label} internal_pipeline.stages entries must be mappings`);
|
|
1941
|
+
continue;
|
|
1942
|
+
}
|
|
1943
|
+
const stageId = String(item.stage_id ?? "");
|
|
1944
|
+
stageIds.add(stageId);
|
|
1945
|
+
for (const key of ["stage_id", "command", "visibility", "status", "outputs"]) {
|
|
1946
|
+
if (!(key in item)) {
|
|
1947
|
+
errors.push(`${label} internal_pipeline stage missing ${key}`);
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
if ((stageId === "vision2prompt" || stageId === "prompt2proto") && item.visibility !== "internal") {
|
|
1951
|
+
errors.push(`${label} internal pipeline stage ${stageId} must be internal`);
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
for (const required of ["proto-preflight", "vision2prompt", "prompt2proto"]) {
|
|
1955
|
+
if (!stageIds.has(required)) {
|
|
1956
|
+
errors.push(`${label} internal_pipeline missing stage ${required}`);
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
function validateDirectionCountPolicy(label, value, errors) {
|
|
1961
|
+
if (!isRecord(value)) {
|
|
1962
|
+
errors.push(`${label} direction_count_policy must be a mapping`);
|
|
1963
|
+
return;
|
|
1964
|
+
}
|
|
1965
|
+
const source = String(value.source ?? "");
|
|
1966
|
+
if (source && !["user_input", "agent_default_after_user_delegation"].includes(source)) {
|
|
1967
|
+
errors.push(`${label} direction_count_policy.source has invalid value ${source}`);
|
|
1968
|
+
}
|
|
1969
|
+
if (typeof value.resolved_count !== "number" || value.resolved_count < 1) {
|
|
1970
|
+
errors.push(`${label} direction_count_policy.resolved_count must be a positive number`);
|
|
1971
|
+
}
|
|
1972
|
+
if (source === "agent_default_after_user_delegation" && value.resolved_count !== 3) {
|
|
1973
|
+
errors.push(`${label} direction_count_policy delegated default must resolve to 3`);
|
|
1974
|
+
}
|
|
1975
|
+
if (value.ask_user_question_required === true && !nonEmptyString(value.ask_user_question)) {
|
|
1976
|
+
errors.push(`${label} direction_count_policy.ask_user_question must be set when askUserQuestion is required`);
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
function validateRequiredObjectFields(label, field, value, keys, errors) {
|
|
1980
|
+
if (!isRecord(value)) {
|
|
1981
|
+
errors.push(`${label} ${field} must be a mapping`);
|
|
1982
|
+
return;
|
|
1983
|
+
}
|
|
1984
|
+
for (const key of keys) {
|
|
1985
|
+
if (!hasUsefulValue(value[key])) {
|
|
1986
|
+
errors.push(`${label} ${field}.${key} must be non-empty`);
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
function validateProductExperienceModel(label, value, data, errors) {
|
|
1991
|
+
if (!strategicPromptPackRequiresProductExperienceModel(data)) {
|
|
1992
|
+
if ("product_experience_model" in data && !isRecord(value)) {
|
|
1993
|
+
errors.push(`${label} product_experience_model must be a mapping when present`);
|
|
1994
|
+
}
|
|
1995
|
+
return;
|
|
1996
|
+
}
|
|
1997
|
+
validateRequiredObjectFields(label, "product_experience_model", value, PRODUCT_EXPERIENCE_MODEL_FIELDS, errors);
|
|
1998
|
+
}
|
|
1999
|
+
function strategicPromptPackRequiresProductExperienceModel(data) {
|
|
2000
|
+
const promptTextManifest = isRecord(data.prompt_text_manifest) ? data.prompt_text_manifest : {};
|
|
2001
|
+
const imageGeneration = isRecord(data.image_generation) ? data.image_generation : {};
|
|
2002
|
+
return (data.status !== "draft" ||
|
|
2003
|
+
promptTextManifest.status === "ready_for_image_generation" ||
|
|
2004
|
+
promptTextManifest.status === "generated" ||
|
|
2005
|
+
(typeof imageGeneration.status === "string" && imageGeneration.status !== "not_started"));
|
|
2006
|
+
}
|
|
2007
|
+
function validatePrototypeSystemContract(label, value, data, errors) {
|
|
2008
|
+
const required = strategicPromptPackRequiresProductExperienceModel(data);
|
|
2009
|
+
if (!required) {
|
|
2010
|
+
if ("prototype_system_contract" in data && !isRecord(value)) {
|
|
2011
|
+
errors.push(`${label} prototype_system_contract must be a mapping when present`);
|
|
2012
|
+
}
|
|
2013
|
+
return;
|
|
2014
|
+
}
|
|
2015
|
+
validateRequiredObjectFields(label, "prototype_system_contract", value, PROTOTYPE_SYSTEM_CONTRACT_FIELDS, errors);
|
|
2016
|
+
if (!isRecord(value)) {
|
|
2017
|
+
return;
|
|
2018
|
+
}
|
|
2019
|
+
for (const key of PROTOTYPE_SYSTEM_CONTRACT_FIELDS) {
|
|
2020
|
+
if (key === "copy_tone") {
|
|
2021
|
+
if (!nonEmptyString(value[key])) {
|
|
2022
|
+
errors.push(`${label} prototype_system_contract.copy_tone must be a non-empty string`);
|
|
2023
|
+
}
|
|
2024
|
+
continue;
|
|
2025
|
+
}
|
|
2026
|
+
if (!Array.isArray(value[key]) || value[key].length === 0) {
|
|
2027
|
+
errors.push(`${label} prototype_system_contract.${key} must be a non-empty array`);
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
function validatePrototypeRealityGate(label, value, data, errors) {
|
|
2032
|
+
const required = strategicPromptPackRequiresProductExperienceModel(data);
|
|
2033
|
+
if (!required) {
|
|
2034
|
+
if ("prototype_reality_gate" in data && !isRecord(value)) {
|
|
2035
|
+
errors.push(`${label} prototype_reality_gate must be a mapping when present`);
|
|
2036
|
+
}
|
|
2037
|
+
return;
|
|
2038
|
+
}
|
|
2039
|
+
if (!isRecord(value)) {
|
|
2040
|
+
errors.push(`${label} prototype_reality_gate must be a mapping`);
|
|
2041
|
+
return;
|
|
2042
|
+
}
|
|
2043
|
+
for (const key of ["status", "trigger", "required_when_prompt_text_ready", "dimensions", "failures", "outcome_notes", "repair_route"]) {
|
|
2044
|
+
if (!(key in value)) {
|
|
2045
|
+
errors.push(`${label} prototype_reality_gate missing ${key}`);
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
const status = String(value.status ?? "");
|
|
2049
|
+
if (status && !["pending", "pass", "fail"].includes(status)) {
|
|
2050
|
+
errors.push(`${label} prototype_reality_gate.status has invalid value ${status}`);
|
|
2051
|
+
}
|
|
2052
|
+
if (value.trigger !== "before_image_generation") {
|
|
2053
|
+
errors.push(`${label} prototype_reality_gate.trigger must be before_image_generation`);
|
|
2054
|
+
}
|
|
2055
|
+
if (value.required_when_prompt_text_ready !== true) {
|
|
2056
|
+
errors.push(`${label} prototype_reality_gate.required_when_prompt_text_ready must be true`);
|
|
2057
|
+
}
|
|
2058
|
+
if (value.repair_route !== "/ow:vision2prompt") {
|
|
2059
|
+
errors.push(`${label} prototype_reality_gate.repair_route must be /ow:vision2prompt`);
|
|
2060
|
+
}
|
|
2061
|
+
if (!Array.isArray(value.dimensions)) {
|
|
2062
|
+
errors.push(`${label} prototype_reality_gate.dimensions must be an array`);
|
|
2063
|
+
}
|
|
2064
|
+
else {
|
|
2065
|
+
for (const dimension of PROTOTYPE_REALITY_GATE_DIMENSIONS) {
|
|
2066
|
+
if (!value.dimensions.includes(dimension)) {
|
|
2067
|
+
errors.push(`${label} prototype_reality_gate.dimensions missing ${dimension}`);
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
for (const key of ["failures", "outcome_notes"]) {
|
|
2072
|
+
if (!Array.isArray(value[key])) {
|
|
2073
|
+
errors.push(`${label} prototype_reality_gate.${key} must be an array`);
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
const promptTextManifest = isRecord(data.prompt_text_manifest) ? data.prompt_text_manifest : {};
|
|
2077
|
+
const imageGeneration = isRecord(data.image_generation) ? data.image_generation : {};
|
|
2078
|
+
const promptTextReady = promptTextManifest.status === "ready_for_image_generation" || promptTextManifest.status === "generated";
|
|
2079
|
+
if (promptTextReady && status !== "pass") {
|
|
2080
|
+
errors.push(`${label} prototype_reality_gate.status must be pass before image generation`);
|
|
2081
|
+
}
|
|
2082
|
+
if (status === "pass" && Array.isArray(value.failures) && value.failures.length > 0) {
|
|
2083
|
+
errors.push(`${label} prototype_reality_gate.failures must be empty when status is pass`);
|
|
2084
|
+
}
|
|
2085
|
+
if (status === "fail" && typeof imageGeneration.status === "string" && imageGeneration.status !== "not_started") {
|
|
2086
|
+
errors.push(`${label} prototype_reality_gate failed gates must not start image_generation`);
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
function validatePromptPackIntegrityGate(label, value, data, errors) {
|
|
2090
|
+
const required = strategicPromptPackRequiresProductExperienceModel(data);
|
|
2091
|
+
if (!required) {
|
|
2092
|
+
if ("prompt_pack_integrity_gate" in data && !isRecord(value)) {
|
|
2093
|
+
errors.push(`${label} prompt_pack_integrity_gate must be a mapping when present`);
|
|
2094
|
+
}
|
|
2095
|
+
return;
|
|
2096
|
+
}
|
|
2097
|
+
if (!isRecord(value)) {
|
|
2098
|
+
errors.push(`${label} prompt_pack_integrity_gate must be a mapping`);
|
|
2099
|
+
return;
|
|
2100
|
+
}
|
|
2101
|
+
for (const key of ["status", "trigger", "required_when_prompt_text_ready", "dimensions", "failures", "outcome_notes", "repair_route"]) {
|
|
2102
|
+
if (!(key in value)) {
|
|
2103
|
+
errors.push(`${label} prompt_pack_integrity_gate missing ${key}`);
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
const status = String(value.status ?? "");
|
|
2107
|
+
if (status && !["pending", "pass", "fail"].includes(status)) {
|
|
2108
|
+
errors.push(`${label} prompt_pack_integrity_gate.status has invalid value ${status}`);
|
|
2109
|
+
}
|
|
2110
|
+
if (value.trigger !== "before_image_generation") {
|
|
2111
|
+
errors.push(`${label} prompt_pack_integrity_gate.trigger must be before_image_generation`);
|
|
2112
|
+
}
|
|
2113
|
+
if (value.required_when_prompt_text_ready !== true) {
|
|
2114
|
+
errors.push(`${label} prompt_pack_integrity_gate.required_when_prompt_text_ready must be true`);
|
|
2115
|
+
}
|
|
2116
|
+
if (value.repair_route !== "/ow:vision2prompt") {
|
|
2117
|
+
errors.push(`${label} prompt_pack_integrity_gate.repair_route must be /ow:vision2prompt`);
|
|
2118
|
+
}
|
|
2119
|
+
if (!Array.isArray(value.dimensions)) {
|
|
2120
|
+
errors.push(`${label} prompt_pack_integrity_gate.dimensions must be an array`);
|
|
2121
|
+
}
|
|
2122
|
+
else {
|
|
2123
|
+
for (const dimension of PROMPT_PACK_INTEGRITY_GATE_DIMENSIONS) {
|
|
2124
|
+
if (!value.dimensions.includes(dimension)) {
|
|
2125
|
+
errors.push(`${label} prompt_pack_integrity_gate.dimensions missing ${dimension}`);
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
2129
|
+
for (const key of ["failures", "outcome_notes"]) {
|
|
2130
|
+
if (!Array.isArray(value[key])) {
|
|
2131
|
+
errors.push(`${label} prompt_pack_integrity_gate.${key} must be an array`);
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2134
|
+
validatePromptPackIntegrity(label, data, errors);
|
|
2135
|
+
const promptTextManifest = isRecord(data.prompt_text_manifest) ? data.prompt_text_manifest : {};
|
|
2136
|
+
const imageGeneration = isRecord(data.image_generation) ? data.image_generation : {};
|
|
2137
|
+
const promptTextReady = promptTextManifest.status === "ready_for_image_generation" || promptTextManifest.status === "generated";
|
|
2138
|
+
if (promptTextReady && status !== "pass") {
|
|
2139
|
+
errors.push(`${label} prompt_pack_integrity_gate.status must be pass before image generation`);
|
|
2140
|
+
}
|
|
2141
|
+
if (status === "pass" && Array.isArray(value.failures) && value.failures.length > 0) {
|
|
2142
|
+
errors.push(`${label} prompt_pack_integrity_gate.failures must be empty when status is pass`);
|
|
2143
|
+
}
|
|
2144
|
+
if (status === "fail" && typeof imageGeneration.status === "string" && imageGeneration.status !== "not_started") {
|
|
2145
|
+
errors.push(`${label} prompt_pack_integrity_gate failed gates must not start image_generation`);
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
function validatePromptPackIntegrity(label, data, errors) {
|
|
2149
|
+
const directions = Array.isArray(data.directions) ? data.directions.filter(isRecord) : [];
|
|
2150
|
+
const directionIds = new Set();
|
|
2151
|
+
const promptIds = new Set();
|
|
2152
|
+
for (const direction of directions) {
|
|
2153
|
+
if (nonEmptyString(direction.direction_id)) {
|
|
2154
|
+
directionIds.add(String(direction.direction_id));
|
|
2155
|
+
}
|
|
2156
|
+
if (Array.isArray(direction.screen_prompts)) {
|
|
2157
|
+
for (const prompt of direction.screen_prompts) {
|
|
2158
|
+
if (isRecord(prompt) && nonEmptyString(prompt.prompt_id)) {
|
|
2159
|
+
promptIds.add(String(prompt.prompt_id));
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
const countPolicy = isRecord(data.direction_count_policy) ? data.direction_count_policy : {};
|
|
2165
|
+
const promptTextManifest = isRecord(data.prompt_text_manifest) ? data.prompt_text_manifest : {};
|
|
2166
|
+
const promptTextReady = promptTextManifest.status === "ready_for_image_generation" || promptTextManifest.status === "generated";
|
|
2167
|
+
const resolvedCount = typeof countPolicy.resolved_count === "number" ? countPolicy.resolved_count : null;
|
|
2168
|
+
if (promptTextReady && resolvedCount !== null && directions.length !== resolvedCount) {
|
|
2169
|
+
errors.push(`${label} directions length must equal direction_count_policy.resolved_count before image generation`);
|
|
2170
|
+
}
|
|
2171
|
+
if (promptTextReady && typeof promptTextManifest.direction_count !== "number") {
|
|
2172
|
+
errors.push(`${label} prompt_text_manifest.direction_count must be a number before image generation`);
|
|
2173
|
+
}
|
|
2174
|
+
if (typeof promptTextManifest.direction_count === "number" && directions.length > 0 && promptTextManifest.direction_count !== directions.length) {
|
|
2175
|
+
errors.push(`${label} prompt_text_manifest.direction_count must equal directions length`);
|
|
2176
|
+
}
|
|
2177
|
+
if (promptTextReady) {
|
|
2178
|
+
const refs = Array.isArray(promptTextManifest.prompt_text_refs) ? promptTextManifest.prompt_text_refs : [];
|
|
2179
|
+
if (refs.length === 0) {
|
|
2180
|
+
errors.push(`${label} prompt_text_manifest.prompt_text_refs must include prompt refs before image generation`);
|
|
2181
|
+
}
|
|
2182
|
+
refs.forEach((ref, index) => {
|
|
2183
|
+
if (!stringReferencesKnownPrompt(String(ref), directionIds, promptIds)) {
|
|
2184
|
+
errors.push(`${label} prompt_text_manifest.prompt_text_refs[${index}] must reference an existing direction_id or prompt_id`);
|
|
2185
|
+
}
|
|
2186
|
+
});
|
|
2187
|
+
}
|
|
2188
|
+
const imageGeneration = isRecord(data.image_generation) ? data.image_generation : {};
|
|
2189
|
+
const generatedImages = Array.isArray(imageGeneration.generated_images) ? imageGeneration.generated_images : [];
|
|
2190
|
+
generatedImages.forEach((image, index) => {
|
|
2191
|
+
if (!isRecord(image)) {
|
|
2192
|
+
return;
|
|
2193
|
+
}
|
|
2194
|
+
const directionId = String(image.direction_id ?? "");
|
|
2195
|
+
const promptId = String(image.prompt_id ?? "");
|
|
2196
|
+
if (directionId && !directionIds.has(directionId)) {
|
|
2197
|
+
errors.push(`${label} image_generation.generated_images[${index}].direction_id must exist in directions`);
|
|
2198
|
+
}
|
|
2199
|
+
if (promptId && !promptIds.has(promptId)) {
|
|
2200
|
+
errors.push(`${label} image_generation.generated_images[${index}].prompt_id must exist in directions[].screen_prompts`);
|
|
2201
|
+
}
|
|
2202
|
+
const metadata = isRecord(image.metadata) ? image.metadata : {};
|
|
2203
|
+
const sourcePromptRef = String(metadata.source_prompt_ref ?? "");
|
|
2204
|
+
if (sourcePromptRef && !stringReferencesKnownPrompt(sourcePromptRef, directionIds, promptIds)) {
|
|
2205
|
+
errors.push(`${label} image_generation.generated_images[${index}].metadata.source_prompt_ref must reference an existing direction_id or prompt_id`);
|
|
2206
|
+
}
|
|
2207
|
+
});
|
|
2208
|
+
}
|
|
2209
|
+
function validateScreenBoundExecutability(label, data, errors) {
|
|
2210
|
+
if (!strategicPromptPackRequiresScreenExecutability(data)) {
|
|
2211
|
+
return;
|
|
2212
|
+
}
|
|
2213
|
+
validateRequiredObjectFields(label, "prototype_brief", data.prototype_brief, STRATEGIC_PROTOTYPE_BRIEF_FIELDS, errors);
|
|
2214
|
+
validateRequiredObjectFields(label, "global_design_system_prompt", data.global_design_system_prompt, STRATEGIC_GLOBAL_DESIGN_SYSTEM_PROMPT_FIELDS, errors);
|
|
2215
|
+
validateRequiredObjectFields(label, "quality_rubric", data.quality_rubric, STRATEGIC_QUALITY_RUBRIC_FIELDS, errors);
|
|
2216
|
+
const manifestIds = validateStrategicScreenManifest(label, data.screen_manifest, errors);
|
|
2217
|
+
validateStrategicDirectionScreenPrompts(label, data.directions, manifestIds, errors);
|
|
2218
|
+
}
|
|
2219
|
+
function strategicPromptPackRequiresScreenExecutability(data) {
|
|
2220
|
+
const promptTextManifest = isRecord(data.prompt_text_manifest) ? data.prompt_text_manifest : {};
|
|
2221
|
+
const imageGeneration = isRecord(data.image_generation) ? data.image_generation : {};
|
|
2222
|
+
return (promptTextManifest.status === "ready_for_image_generation" ||
|
|
2223
|
+
promptTextManifest.status === "generated" ||
|
|
2224
|
+
(typeof imageGeneration.status === "string" && imageGeneration.status !== "not_started"));
|
|
2225
|
+
}
|
|
2226
|
+
function validateStrategicScreenManifest(label, value, errors) {
|
|
2227
|
+
const ids = new Set();
|
|
2228
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
2229
|
+
errors.push(`${label} screen_manifest must contain screen-bound product states before image generation`);
|
|
2230
|
+
return ids;
|
|
2231
|
+
}
|
|
2232
|
+
value.forEach((item, index) => {
|
|
2233
|
+
validateRequiredObjectFields(label, `screen_manifest[${index}]`, item, STRATEGIC_SCREEN_MANIFEST_FIELDS, errors);
|
|
2234
|
+
if (!isRecord(item)) {
|
|
2235
|
+
return;
|
|
369
2236
|
}
|
|
2237
|
+
if (nonEmptyString(item.target_screen_id)) {
|
|
2238
|
+
ids.add(String(item.target_screen_id));
|
|
2239
|
+
}
|
|
2240
|
+
if (!hasUsefulValue(item.ai_behavior) && !hasUsefulValue(item.non_ai_rationale)) {
|
|
2241
|
+
errors.push(`${label} screen_manifest[${index}] must include ai_behavior or non_ai_rationale`);
|
|
2242
|
+
}
|
|
2243
|
+
});
|
|
2244
|
+
return ids;
|
|
2245
|
+
}
|
|
2246
|
+
function validateStrategicDirectionScreenPrompts(label, directions, manifestIds, errors) {
|
|
2247
|
+
if (!Array.isArray(directions)) {
|
|
2248
|
+
return;
|
|
370
2249
|
}
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
2250
|
+
directions.forEach((direction, directionIndex) => {
|
|
2251
|
+
if (!isRecord(direction)) {
|
|
2252
|
+
return;
|
|
2253
|
+
}
|
|
2254
|
+
const screenPrompts = direction.screen_prompts;
|
|
2255
|
+
if (!Array.isArray(screenPrompts) || screenPrompts.length === 0) {
|
|
2256
|
+
errors.push(`${label} directions[${directionIndex}].screen_prompts must contain screen-bound prompt text before image generation`);
|
|
2257
|
+
return;
|
|
2258
|
+
}
|
|
2259
|
+
screenPrompts.forEach((prompt, promptIndex) => {
|
|
2260
|
+
const fieldLabel = `directions[${directionIndex}].screen_prompts[${promptIndex}]`;
|
|
2261
|
+
validateRequiredObjectFields(label, fieldLabel, prompt, STRATEGIC_SCREEN_PROMPT_FIELDS, errors);
|
|
2262
|
+
if (!isRecord(prompt)) {
|
|
2263
|
+
return;
|
|
2264
|
+
}
|
|
2265
|
+
if (!hasUsefulValue(prompt.prompt) && !hasUsefulValue(prompt.standalone_prompt)) {
|
|
2266
|
+
errors.push(`${label} ${fieldLabel} must include prompt or standalone_prompt`);
|
|
379
2267
|
}
|
|
2268
|
+
if (nonEmptyString(prompt.target_screen_id) && manifestIds.size > 0 && !manifestIds.has(String(prompt.target_screen_id))) {
|
|
2269
|
+
errors.push(`${label} ${fieldLabel}.target_screen_id must exist in screen_manifest`);
|
|
2270
|
+
}
|
|
2271
|
+
});
|
|
2272
|
+
});
|
|
2273
|
+
}
|
|
2274
|
+
function stringReferencesKnownPrompt(value, directionIds, promptIds) {
|
|
2275
|
+
const normalized = normalizePromptRef(value);
|
|
2276
|
+
if (normalized.length === 0) {
|
|
2277
|
+
return false;
|
|
2278
|
+
}
|
|
2279
|
+
for (const id of [...directionIds, ...promptIds]) {
|
|
2280
|
+
const token = normalizePromptRef(id);
|
|
2281
|
+
if (token.length > 0 && normalized.includes(token)) {
|
|
2282
|
+
return true;
|
|
380
2283
|
}
|
|
381
2284
|
}
|
|
382
|
-
|
|
383
|
-
errors.push(`${label} artifact ${index} active_pointer must be a mapping`);
|
|
384
|
-
}
|
|
2285
|
+
return false;
|
|
385
2286
|
}
|
|
386
|
-
function
|
|
387
|
-
|
|
2287
|
+
function normalizePromptRef(value) {
|
|
2288
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "");
|
|
2289
|
+
}
|
|
2290
|
+
function validateStrategicDirections(label, value, countPolicy, errors) {
|
|
2291
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
2292
|
+
errors.push(`${label} directions must contain strategic prompt directions`);
|
|
388
2293
|
return;
|
|
389
2294
|
}
|
|
390
|
-
const
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
errors.push(`${label} must contain disclosure levels 0 through 4`);
|
|
394
|
-
return;
|
|
2295
|
+
const resolvedCount = isRecord(countPolicy) && typeof countPolicy.resolved_count === "number" ? countPolicy.resolved_count : null;
|
|
2296
|
+
if (resolvedCount !== null && value.length < resolvedCount) {
|
|
2297
|
+
errors.push(`${label} directions must include at least direction_count_policy.resolved_count items`);
|
|
395
2298
|
}
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
errors.push(`${label} level ${index} is not a mapping`);
|
|
2299
|
+
value.forEach((item, index) => {
|
|
2300
|
+
if (!isRecord(item)) {
|
|
2301
|
+
errors.push(`${label} directions[${index}] must be a mapping`);
|
|
400
2302
|
return;
|
|
401
2303
|
}
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
for (const key of ["name", "default_for_agents", "purpose", "examples"]) {
|
|
406
|
-
if (!(key in level)) {
|
|
407
|
-
errors.push(`${label} level ${index} missing ${key}`);
|
|
2304
|
+
for (const key of STRATEGIC_DIRECTION_FIELDS) {
|
|
2305
|
+
if (!hasUsefulValue(item[key])) {
|
|
2306
|
+
errors.push(`${label} directions[${index}].${key} must be non-empty`);
|
|
408
2307
|
}
|
|
409
2308
|
}
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
errors.push(`${label} missing disclosure level ${level}`);
|
|
2309
|
+
const screenPrompts = item.screen_prompts;
|
|
2310
|
+
if (Array.isArray(screenPrompts) && screenPrompts.length < 2) {
|
|
2311
|
+
errors.push(`${label} directions[${index}].screen_prompts must include multi-image prompt text`);
|
|
414
2312
|
}
|
|
415
|
-
|
|
2313
|
+
const distinctness = String(item.distinctness_rationale ?? "").toLowerCase();
|
|
2314
|
+
if (!STRATEGIC_DISTINCTNESS_SIGNALS.some((signal) => distinctness.includes(signal))) {
|
|
2315
|
+
errors.push(`${label} directions[${index}].distinctness_rationale must name a strategic difference, not only visual style`);
|
|
2316
|
+
}
|
|
2317
|
+
});
|
|
416
2318
|
}
|
|
417
|
-
function
|
|
418
|
-
|
|
2319
|
+
function validateBuildRecommendation(label, value, errors) {
|
|
2320
|
+
validateRequiredObjectFields(label, "build_recommendation", value, ["first_direction_id", "why_first", "success_signals", "failure_signals", "next_test_if_it_works"], errors);
|
|
2321
|
+
}
|
|
2322
|
+
function validatePromptTextManifest(label, value, errors) {
|
|
2323
|
+
if (!isRecord(value)) {
|
|
2324
|
+
errors.push(`${label} prompt_text_manifest must be a mapping`);
|
|
419
2325
|
return;
|
|
420
2326
|
}
|
|
421
|
-
const
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
2327
|
+
const status = String(value.status ?? "");
|
|
2328
|
+
if (status && !["draft", "ready_for_image_generation", "generated"].includes(status)) {
|
|
2329
|
+
errors.push(`${label} prompt_text_manifest.status has invalid value ${status}`);
|
|
2330
|
+
}
|
|
2331
|
+
if (status !== "draft" && value.directions_ready !== true) {
|
|
2332
|
+
errors.push(`${label} prompt_text_manifest.directions_ready must be true before image generation`);
|
|
2333
|
+
}
|
|
2334
|
+
if (!Array.isArray(value.prompt_text_refs)) {
|
|
2335
|
+
errors.push(`${label} prompt_text_manifest.prompt_text_refs must be an array`);
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
function validatePostValidate(label, value, countPolicy, promptTextManifest, imageGeneration, directions, errors) {
|
|
2339
|
+
if (!isRecord(value)) {
|
|
2340
|
+
errors.push(`${label} post_validate must be a mapping`);
|
|
425
2341
|
return;
|
|
426
2342
|
}
|
|
427
|
-
for (const key of
|
|
428
|
-
if (!(key in
|
|
429
|
-
errors.push(`${label} missing
|
|
2343
|
+
for (const key of ["status", "trigger", "required_when_direction_count_gte", "skip_when_resolved_count", "threshold_policy", "fingerprint_dimensions", "comparisons", "failures", "outcome_notes", "repair_route"]) {
|
|
2344
|
+
if (!(key in value)) {
|
|
2345
|
+
errors.push(`${label} post_validate missing ${key}`);
|
|
430
2346
|
}
|
|
431
2347
|
}
|
|
432
|
-
|
|
433
|
-
|
|
2348
|
+
const status = String(value.status ?? "");
|
|
2349
|
+
if (status && !["pending", "pass", "fail", "skipped"].includes(status)) {
|
|
2350
|
+
errors.push(`${label} post_validate.status has invalid value ${status}`);
|
|
434
2351
|
}
|
|
435
|
-
|
|
436
|
-
|
|
2352
|
+
if (value.trigger !== "after_prompt_assets_ready") {
|
|
2353
|
+
errors.push(`${label} post_validate.trigger must be after_prompt_assets_ready`);
|
|
437
2354
|
}
|
|
438
|
-
|
|
439
|
-
|
|
2355
|
+
if (value.required_when_direction_count_gte !== 2) {
|
|
2356
|
+
errors.push(`${label} post_validate.required_when_direction_count_gte must be 2`);
|
|
440
2357
|
}
|
|
441
|
-
|
|
442
|
-
|
|
2358
|
+
if (value.skip_when_resolved_count !== 1) {
|
|
2359
|
+
errors.push(`${label} post_validate.skip_when_resolved_count must be 1`);
|
|
443
2360
|
}
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
"verification",
|
|
470
|
-
"self_critique",
|
|
471
|
-
"known_limits",
|
|
472
|
-
"result",
|
|
473
|
-
"handoff",
|
|
474
|
-
],
|
|
475
|
-
decision_record: [
|
|
476
|
-
"reviewed_evidence",
|
|
477
|
-
"outcome",
|
|
478
|
-
"rationale",
|
|
479
|
-
"accepted_scope",
|
|
480
|
-
"rejected_scope",
|
|
481
|
-
"revision_scope",
|
|
482
|
-
"next_command",
|
|
483
|
-
"follow_up_questions",
|
|
484
|
-
],
|
|
485
|
-
product_design: [
|
|
486
|
-
"accepted_prototype_evidence",
|
|
487
|
-
"personas",
|
|
488
|
-
"journey_map",
|
|
489
|
-
"user_stories",
|
|
490
|
-
"feature_matrix",
|
|
491
|
-
"kano_classification",
|
|
492
|
-
"behavior_model",
|
|
493
|
-
"ux_states",
|
|
494
|
-
"scope",
|
|
495
|
-
"open_questions",
|
|
496
|
-
"conditional_packets",
|
|
497
|
-
"spec_readiness",
|
|
498
|
-
],
|
|
499
|
-
};
|
|
500
|
-
return requiredByType[artifactType] ?? null;
|
|
501
|
-
}
|
|
502
|
-
function validateValidationTarget(label, data, errors) {
|
|
503
|
-
const featureClassification = data.feature_classification;
|
|
504
|
-
if (isRecord(featureClassification)) {
|
|
505
|
-
for (const key of ["existential", "supporting", "later", "out_of_scope"]) {
|
|
506
|
-
if (!(key in featureClassification)) {
|
|
507
|
-
errors.push(`${label} feature_classification missing ${key}`);
|
|
2361
|
+
if (value.repair_route !== "/ow:vision2prompt") {
|
|
2362
|
+
errors.push(`${label} post_validate.repair_route must be /ow:vision2prompt`);
|
|
2363
|
+
}
|
|
2364
|
+
const thresholdPolicy = value.threshold_policy;
|
|
2365
|
+
if (!isRecord(thresholdPolicy)) {
|
|
2366
|
+
errors.push(`${label} post_validate.threshold_policy must be a mapping`);
|
|
2367
|
+
}
|
|
2368
|
+
else {
|
|
2369
|
+
if (thresholdPolicy.method !== "strategic_fingerprint_similarity") {
|
|
2370
|
+
errors.push(`${label} post_validate.threshold_policy.method must be strategic_fingerprint_similarity`);
|
|
2371
|
+
}
|
|
2372
|
+
if (thresholdPolicy.comparison !== "pairwise") {
|
|
2373
|
+
errors.push(`${label} post_validate.threshold_policy.comparison must be pairwise`);
|
|
2374
|
+
}
|
|
2375
|
+
if (typeof thresholdPolicy.max_pairwise_similarity !== "number" || thresholdPolicy.max_pairwise_similarity <= 0 || thresholdPolicy.max_pairwise_similarity >= 1) {
|
|
2376
|
+
errors.push(`${label} post_validate.threshold_policy.max_pairwise_similarity must be between 0 and 1`);
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
2379
|
+
if (!Array.isArray(value.fingerprint_dimensions)) {
|
|
2380
|
+
errors.push(`${label} post_validate.fingerprint_dimensions must be an array`);
|
|
2381
|
+
}
|
|
2382
|
+
else {
|
|
2383
|
+
for (const dimension of STRATEGIC_FINGERPRINT_DIMENSIONS) {
|
|
2384
|
+
if (!value.fingerprint_dimensions.includes(dimension)) {
|
|
2385
|
+
errors.push(`${label} post_validate.fingerprint_dimensions missing ${dimension}`);
|
|
508
2386
|
}
|
|
509
2387
|
}
|
|
510
2388
|
}
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
return;
|
|
2389
|
+
for (const key of ["comparisons", "failures", "outcome_notes"]) {
|
|
2390
|
+
if (!Array.isArray(value[key])) {
|
|
2391
|
+
errors.push(`${label} post_validate.${key} must be an array`);
|
|
2392
|
+
}
|
|
516
2393
|
}
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
2394
|
+
const resolvedCount = isRecord(countPolicy) && typeof countPolicy.resolved_count === "number" ? countPolicy.resolved_count : null;
|
|
2395
|
+
const promptStatus = isRecord(promptTextManifest) ? String(promptTextManifest.status ?? "") : "";
|
|
2396
|
+
const promptReady = promptStatus === "ready_for_image_generation" || promptStatus === "generated";
|
|
2397
|
+
if (resolvedCount === 1 && status !== "skipped") {
|
|
2398
|
+
errors.push(`${label} post_validate.status must be skipped when direction_count_policy.resolved_count is 1`);
|
|
2399
|
+
}
|
|
2400
|
+
if (promptReady && resolvedCount !== null && resolvedCount >= 2 && !["pass", "fail"].includes(status)) {
|
|
2401
|
+
errors.push(`${label} post_validate.status must be pass or fail before /ow:prompt2proto when resolved_count is 2 or more`);
|
|
2402
|
+
}
|
|
2403
|
+
if (status === "skipped" && resolvedCount !== 1) {
|
|
2404
|
+
errors.push(`${label} post_validate.status can be skipped only when resolved_count is 1`);
|
|
2405
|
+
}
|
|
2406
|
+
if (status === "fail") {
|
|
2407
|
+
const imageStatus = isRecord(imageGeneration) ? String(imageGeneration.status ?? "") : "";
|
|
2408
|
+
if (["queued", "in_progress", "complete"].includes(imageStatus)) {
|
|
2409
|
+
errors.push(`${label} post_validate failed gates must not start image_generation`);
|
|
520
2410
|
}
|
|
521
2411
|
}
|
|
2412
|
+
if (status === "pass" && promptReady && resolvedCount !== null && resolvedCount >= 2) {
|
|
2413
|
+
validateStrategicFingerprintSimilarity(label, directions, value.fingerprint_dimensions, thresholdPolicy, errors);
|
|
2414
|
+
}
|
|
522
2415
|
}
|
|
523
|
-
function
|
|
524
|
-
if (!Array.isArray(
|
|
525
|
-
errors.push(`${label} must contain non-empty todo list`);
|
|
2416
|
+
function validateStrategicFingerprintSimilarity(label, directions, dimensions, thresholdPolicy, errors) {
|
|
2417
|
+
if (!Array.isArray(directions) || !Array.isArray(dimensions) || !isRecord(thresholdPolicy)) {
|
|
526
2418
|
return;
|
|
527
2419
|
}
|
|
528
|
-
const
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
if (seen.has(taskId)) {
|
|
540
|
-
errors.push(`${label} duplicate prototype task_id ${taskId}`);
|
|
2420
|
+
const threshold = typeof thresholdPolicy.max_pairwise_similarity === "number" ? thresholdPolicy.max_pairwise_similarity : null;
|
|
2421
|
+
if (threshold === null) {
|
|
2422
|
+
return;
|
|
2423
|
+
}
|
|
2424
|
+
const records = directions
|
|
2425
|
+
.filter(isRecord)
|
|
2426
|
+
.map((direction, index) => {
|
|
2427
|
+
const directionId = nonEmptyString(direction.direction_id) ? String(direction.direction_id) : `index-${index}`;
|
|
2428
|
+
const fingerprint = isRecord(direction.strategic_fingerprint) ? direction.strategic_fingerprint : null;
|
|
2429
|
+
if (fingerprint === null) {
|
|
2430
|
+
errors.push(`${label} directions[${index}].strategic_fingerprint must be set when post_validate.status is pass`);
|
|
541
2431
|
}
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
2432
|
+
return { directionId, index, fingerprint };
|
|
2433
|
+
});
|
|
2434
|
+
for (let left = 0; left < records.length; left += 1) {
|
|
2435
|
+
for (let right = left + 1; right < records.length; right += 1) {
|
|
2436
|
+
const leftRecord = records[left];
|
|
2437
|
+
const rightRecord = records[right];
|
|
2438
|
+
if (!leftRecord?.fingerprint || !rightRecord?.fingerprint) {
|
|
2439
|
+
continue;
|
|
2440
|
+
}
|
|
2441
|
+
const result = compareStrategicFingerprints(leftRecord.fingerprint, rightRecord.fingerprint, dimensions);
|
|
2442
|
+
if (result.comparedCount === 0) {
|
|
2443
|
+
errors.push(`${label} post_validate cannot compare ${leftRecord.directionId} and ${rightRecord.directionId}: no populated strategic_fingerprint dimensions`);
|
|
2444
|
+
continue;
|
|
2445
|
+
}
|
|
2446
|
+
if (result.score > threshold) {
|
|
2447
|
+
errors.push(`${label} post_validate pair ${leftRecord.directionId}/${rightRecord.directionId} exceeds strategic fingerprint similarity threshold ${threshold}: score ${formatSimilarity(result.score)} shared dimensions ${result.sharedDimensions.join(", ")}`);
|
|
546
2448
|
}
|
|
547
2449
|
}
|
|
548
|
-
}
|
|
2450
|
+
}
|
|
549
2451
|
}
|
|
550
|
-
function
|
|
551
|
-
|
|
552
|
-
|
|
2452
|
+
function compareStrategicFingerprints(left, right, dimensions) {
|
|
2453
|
+
let total = 0;
|
|
2454
|
+
let comparedCount = 0;
|
|
2455
|
+
const sharedDimensions = [];
|
|
2456
|
+
for (const rawDimension of dimensions) {
|
|
2457
|
+
const dimension = String(rawDimension);
|
|
2458
|
+
const leftTokens = fingerprintTokens(left[dimension]);
|
|
2459
|
+
const rightTokens = fingerprintTokens(right[dimension]);
|
|
2460
|
+
if (leftTokens.size === 0 || rightTokens.size === 0) {
|
|
2461
|
+
continue;
|
|
2462
|
+
}
|
|
2463
|
+
const similarity = jaccardSimilarity(leftTokens, rightTokens);
|
|
2464
|
+
total += similarity;
|
|
2465
|
+
comparedCount += 1;
|
|
2466
|
+
if (similarity >= 0.8) {
|
|
2467
|
+
sharedDimensions.push(dimension);
|
|
2468
|
+
}
|
|
553
2469
|
}
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
2470
|
+
return {
|
|
2471
|
+
score: comparedCount === 0 ? 0 : total / comparedCount,
|
|
2472
|
+
comparedCount,
|
|
2473
|
+
sharedDimensions,
|
|
2474
|
+
};
|
|
2475
|
+
}
|
|
2476
|
+
function fingerprintTokens(value) {
|
|
2477
|
+
const raw = Array.isArray(value)
|
|
2478
|
+
? value.join(" ")
|
|
2479
|
+
: isRecord(value)
|
|
2480
|
+
? Object.values(value).join(" ")
|
|
2481
|
+
: String(value ?? "");
|
|
2482
|
+
return new Set(raw.toLowerCase().split(/[^a-z0-9]+/).filter((token) => token.length > 1));
|
|
2483
|
+
}
|
|
2484
|
+
function jaccardSimilarity(left, right) {
|
|
2485
|
+
let intersection = 0;
|
|
2486
|
+
for (const token of left) {
|
|
2487
|
+
if (right.has(token)) {
|
|
2488
|
+
intersection += 1;
|
|
557
2489
|
}
|
|
558
2490
|
}
|
|
559
|
-
|
|
560
|
-
|
|
2491
|
+
const union = left.size + right.size - intersection;
|
|
2492
|
+
return union === 0 ? 0 : intersection / union;
|
|
2493
|
+
}
|
|
2494
|
+
function formatSimilarity(value) {
|
|
2495
|
+
return value.toFixed(2);
|
|
2496
|
+
}
|
|
2497
|
+
function validateImageGeneration(label, value, errors) {
|
|
2498
|
+
if (!isRecord(value)) {
|
|
2499
|
+
errors.push(`${label} image_generation must be a mapping`);
|
|
2500
|
+
return;
|
|
561
2501
|
}
|
|
562
|
-
|
|
563
|
-
if ("
|
|
564
|
-
errors.push(`${label}
|
|
2502
|
+
const status = String(value.status ?? "");
|
|
2503
|
+
if (status && !["not_started", "queued", "in_progress", "complete", "blocked"].includes(status)) {
|
|
2504
|
+
errors.push(`${label} image_generation.status has invalid value ${status}`);
|
|
565
2505
|
}
|
|
566
|
-
if (
|
|
567
|
-
errors.push(`${label}
|
|
2506
|
+
if (!nonEmptyString(value.batch_strategy)) {
|
|
2507
|
+
errors.push(`${label} image_generation.batch_strategy must be non-empty`);
|
|
2508
|
+
}
|
|
2509
|
+
if (!Array.isArray(value.generated_images)) {
|
|
2510
|
+
errors.push(`${label} image_generation.generated_images must be an array`);
|
|
568
2511
|
}
|
|
569
2512
|
else {
|
|
570
|
-
|
|
2513
|
+
value.generated_images.forEach((item, index) => validateGeneratedImageMetadata(label, item, index, errors));
|
|
571
2514
|
}
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
2515
|
+
if (!Array.isArray(value.collection_notes)) {
|
|
2516
|
+
errors.push(`${label} image_generation.collection_notes must be an array`);
|
|
2517
|
+
}
|
|
2518
|
+
}
|
|
2519
|
+
function validateGeneratedImageMetadata(label, value, index, errors) {
|
|
2520
|
+
if (!isRecord(value)) {
|
|
2521
|
+
errors.push(`${label} image_generation.generated_images[${index}] must be a mapping`);
|
|
2522
|
+
return;
|
|
2523
|
+
}
|
|
2524
|
+
for (const key of ["image_id", "direction_id", "prompt_id", "screen_name", "path", "metadata"]) {
|
|
2525
|
+
if (!hasUsefulValue(value[key])) {
|
|
2526
|
+
errors.push(`${label} image_generation.generated_images[${index}].${key} must be non-empty`);
|
|
578
2527
|
}
|
|
579
|
-
validateLocalRef(root, label, "prototype_artifact.path", prototypeArtifact.path, errors);
|
|
580
2528
|
}
|
|
581
|
-
|
|
582
|
-
if (!
|
|
583
|
-
errors.push(`${label}
|
|
2529
|
+
const metadata = value.metadata;
|
|
2530
|
+
if (!isRecord(metadata)) {
|
|
2531
|
+
errors.push(`${label} image_generation.generated_images[${index}].metadata must be a mapping`);
|
|
2532
|
+
return;
|
|
2533
|
+
}
|
|
2534
|
+
for (const key of ["source_prompt_ref", "generated_at", "generator", "generation_status", "review_status"]) {
|
|
2535
|
+
if (!hasUsefulValue(metadata[key])) {
|
|
2536
|
+
errors.push(`${label} image_generation.generated_images[${index}].metadata.${key} must be non-empty`);
|
|
2537
|
+
}
|
|
2538
|
+
}
|
|
2539
|
+
}
|
|
2540
|
+
function hasUsefulValue(value) {
|
|
2541
|
+
if (Array.isArray(value)) {
|
|
2542
|
+
return value.length > 0;
|
|
2543
|
+
}
|
|
2544
|
+
if (isRecord(value)) {
|
|
2545
|
+
return Object.keys(value).length > 0;
|
|
584
2546
|
}
|
|
2547
|
+
return nonEmptyString(value);
|
|
585
2548
|
}
|
|
586
2549
|
function validateVisualConceptPolicy(label, data, errors) {
|
|
587
2550
|
const policy = data.visual_concept_policy;
|
|
@@ -644,20 +2607,23 @@ function validateEvidenceRefs(root, label, data, errors) {
|
|
|
644
2607
|
}
|
|
645
2608
|
}
|
|
646
2609
|
function validateLocalRef(root, label, field, value, errors) {
|
|
647
|
-
if (typeof value !== "string"
|
|
2610
|
+
if (typeof value !== "string") {
|
|
648
2611
|
return;
|
|
649
2612
|
}
|
|
650
|
-
const
|
|
651
|
-
if (
|
|
2613
|
+
const ref = resolveLocalReference(root, value, { exists: existsSyncSafe });
|
|
2614
|
+
if (ref.kind === "empty" || ref.kind === "external") {
|
|
2615
|
+
return;
|
|
2616
|
+
}
|
|
2617
|
+
if (ref.kind === "outside-root") {
|
|
652
2618
|
errors.push(`${label} ${field} references path outside root: ${value}`);
|
|
653
2619
|
return;
|
|
654
2620
|
}
|
|
655
|
-
if (!
|
|
2621
|
+
if (!ref.exists) {
|
|
656
2622
|
errors.push(`${label} ${field} references missing path ${value}`);
|
|
657
2623
|
}
|
|
658
2624
|
}
|
|
659
2625
|
function isExternalRef(value) {
|
|
660
|
-
return
|
|
2626
|
+
return isExternalReference(value);
|
|
661
2627
|
}
|
|
662
2628
|
function nonEmptyString(value) {
|
|
663
2629
|
return typeof value === "string" && value.trim().length > 0;
|
|
@@ -677,6 +2643,52 @@ function validateProductDesign(label, data, errors) {
|
|
|
677
2643
|
}
|
|
678
2644
|
}
|
|
679
2645
|
}
|
|
2646
|
+
function validateProductionSpec(label, data, errors) {
|
|
2647
|
+
for (const key of ["scope", "requirements", "interfaces", "verification", "change_readiness"]) {
|
|
2648
|
+
if (key in data && !isRecord(data[key])) {
|
|
2649
|
+
errors.push(`${label} ${key} must be a mapping`);
|
|
2650
|
+
}
|
|
2651
|
+
}
|
|
2652
|
+
const changeReadiness = data.change_readiness;
|
|
2653
|
+
if (isRecord(changeReadiness)) {
|
|
2654
|
+
for (const key of ["ready", "next_command"]) {
|
|
2655
|
+
if (!(key in changeReadiness)) {
|
|
2656
|
+
errors.push(`${label} change_readiness missing ${key}`);
|
|
2657
|
+
}
|
|
2658
|
+
}
|
|
2659
|
+
}
|
|
2660
|
+
}
|
|
2661
|
+
function validateProductionChange(label, data, errors) {
|
|
2662
|
+
for (const key of ["goals", "non_goals", "affected_paths", "acceptance", "validation", "risks"]) {
|
|
2663
|
+
if (key in data && !Array.isArray(data[key])) {
|
|
2664
|
+
errors.push(`${label} ${key} must be an array`);
|
|
2665
|
+
}
|
|
2666
|
+
}
|
|
2667
|
+
const runtimeReadiness = data.runtime_readiness;
|
|
2668
|
+
if (isRecord(runtimeReadiness)) {
|
|
2669
|
+
for (const key of ["ready", "next_command"]) {
|
|
2670
|
+
if (!(key in runtimeReadiness)) {
|
|
2671
|
+
errors.push(`${label} runtime_readiness missing ${key}`);
|
|
2672
|
+
}
|
|
2673
|
+
}
|
|
2674
|
+
}
|
|
2675
|
+
}
|
|
2676
|
+
function validateTeamRuntime(label, data, errors) {
|
|
2677
|
+
if (typeof data.execution_mode === "string" && !["single_agent", "agent_team", "reconcile", "qa_fix"].includes(data.execution_mode)) {
|
|
2678
|
+
errors.push(`${label} has invalid execution_mode ${data.execution_mode}`);
|
|
2679
|
+
}
|
|
2680
|
+
for (const key of ["work_queue", "agents", "issues", "checkpoints"]) {
|
|
2681
|
+
if (key in data && !Array.isArray(data[key])) {
|
|
2682
|
+
errors.push(`${label} ${key} must be an array`);
|
|
2683
|
+
}
|
|
2684
|
+
}
|
|
2685
|
+
if ("verification" in data && !isRecord(data.verification)) {
|
|
2686
|
+
errors.push(`${label} verification must be a mapping`);
|
|
2687
|
+
}
|
|
2688
|
+
if ("handoff" in data && !isRecord(data.handoff)) {
|
|
2689
|
+
errors.push(`${label} handoff must be a mapping`);
|
|
2690
|
+
}
|
|
2691
|
+
}
|
|
680
2692
|
function validateActivePointer(root, path, data, errors) {
|
|
681
2693
|
const rule = [
|
|
682
2694
|
pointerRule("VISION_CONTRACT.yaml", "current_session", "sessions", "session_id", "path"),
|
|
@@ -684,6 +2696,9 @@ function validateActivePointer(root, path, data, errors) {
|
|
|
684
2696
|
pointerRule("PROTOTYPE_INDEX.yaml", "current_prototype", "prototypes", "prototype_id", "path"),
|
|
685
2697
|
pointerRule("DECISION_INDEX.yaml", "current_decision", "decisions", "decision_id", "path"),
|
|
686
2698
|
pointerRule("DESIGN_INDEX.yaml", "current_design", "designs", "design_id", "path"),
|
|
2699
|
+
pointerRule("SPEC_INDEX.yaml", "current_spec", "specs", "spec_id", "path"),
|
|
2700
|
+
pointerRule("CHANGE_INDEX.yaml", "current_change", "changes", "change_id", "path"),
|
|
2701
|
+
pointerRule("RUNTIME_INDEX.yaml", "current_run", "runs", "run_id", "path"),
|
|
687
2702
|
].find((item) => basename(path) === item.fileName);
|
|
688
2703
|
if (!rule) {
|
|
689
2704
|
return;
|